2

如何编写一个将 const char[N]s 和 std::strings 作为参数的可变参数模板,但根据参数类型执行不同的行为?

到目前为止,我的可变参数模板如下所示:

template<typename T>
void Capitalize_And_Output(T&& str) {
    std::transform(str.begin(), str.end(), str.begin(), ::toupper); //<- will not compile with char*s
    std::cout << str << std::endl;  
    return;
}

template<typename First, typename ... Strings>
void Capitalize_And_Output(First&& str, Strings&&... rest) {
    std::transform(str.begin(), str.end(), str.begin(), ::toupper); //<- will not compile with char*s
    std::cout << str << " ";
    Capitalize_And_Output(std::forward<Strings>(rest)...);
    return;
}

使用“通用”引用,所有内容都被接受到函数中。
但是,像这样调用函数将不起作用:

std::string hello = "hello";
std::string earth = "earth";

//fails because "planet" is a const char[N] and not a std::string
Capitalize_And_Output(hello,"planet","earth"); //outputs: "HELLO PLANET EARTH"

如果我执行以下操作,它确实有效:

Capitalize_And_Output(hello,std::string("planet"),"earth"); //outputs: "HELLO PLANET EARTH"

但我不希望用户负责进行这种转换。我怎样才能把这个职责传递给模板函数呢?

我一直在尝试使用类型特征做出决定,但没有成功。我试图使用:

std::is_same<First, std::string&>::value   

但不知道如何做出分支决定。我不相信这在 if 语句中有效。

也许我需要以某种方式使用 std::conditional ?也许我需要通过在模板中创建一个 auto&& 类型的局部变量来解决它?到目前为止,我在尝试过的各种事情上都没有取得任何成功。

4

4 回答 4

4

我看到Simple解决方案有两个问题:

(1) 编译这个测试用例失败

std::string hello = "hello";
const std::string earth = "earth";
Capitalize_And_Output(hello, "planet", earth);

因为earth是 aconst std::string并且没有可以接这个电话的重载。(试试看!)

(2) 对于可转换为 的类型(除了const char*和类似的),它无法编译std::string,例如,

struct beautiful {
    operator std::string() const {
        return "beautiful";
    }
};

Capitalize_And_Output(hello, beautiful{}, "planet", earth);

以下实现解决了这些问题:

新解决方案:我的旧解决方案(如下)有效,但对char*, char[N]. 此外,它很复杂,并且使用了一些重载解析技巧来避免歧义。这个更简单,更有效。

void Capitalize_And_Output_impl(const char* str) {
    while (char c = toupper(*str++))
        std::cout << c;
}

void Capitalize_And_Output_impl(std::string& str) {
    std::transform(str.begin(), str.end(), str.begin(), toupper);
    std::cout << str;
}

void Capitalize_And_Output_impl(const std::string& str) {
    Capitalize_And_Output_impl(str.data());
}

template<typename First>
void Capitalize_And_Output(First&& str) {
    Capitalize_And_Output_impl(std::forward<First>(str));
    std::cout << '\n';
}

template<typename First, typename ... Strings>
void Capitalize_And_Output(First&& str, Strings&&... rest) {
    Capitalize_And_Output_impl(std::forward<First>(str));
    std::cout << ' ';
    Capitalize_And_Output(std::forward<Strings>(rest)...);
}

因为我不使用std::transform(第二次重载除外),所以不需要提前知道字符串的大小。因此,对于 achar*没有必要调用std::strlen(如在其他解决方案中)。

需要注意的一个小细节是这个实现只打印单词之间的空格。(它不会在最后一个单词之后打印一个。)

旧解决方案

void Capitalize_And_Output_impl(std::string& str, int) {
    std::transform(str.begin(), str.end(), str.begin(), ::toupper);
    std::cout << str << ' ';
}

void Capitalize_And_Output_impl(std::string str, long) {
    Capitalize_And_Output_impl(str, 0);
}

void Capitalize_And_Output() {
    std::cout << '\n';
}

template<typename First, typename ... Strings>
void Capitalize_And_Output(First&& str, Strings&&... rest) {
    Capitalize_And_Output_impl(std::forward<First>(str), 0);
    Capitalize_And_Output(std::forward<Strings>(rest)...);
}

我想这两个Capitalize_And_Output_impl重载值得解释。

首先考虑第二个参数(int/ long)。第一个重载可以采用const在退出时大写的非左值(正如 Trevor Hickney 在对 Simple 的解决方案的评论中所要求的那样)。

第二个超载意味着接受其他所有内容,即右值和左值const。这个想法是将参数复制到一个左值,然后将其传递给第一个重载。这个函数自然可以这样实现(仍然不考虑第二个参数):

void Capitalize_And_Output_impl(const std::string& str) {
    std::string tmp(str);
    Capitalize_And_Output_impl(tmp);
}

这项工作按要求进行。但是,Dave Abrahams 的一篇著名文章解释说,当您通过引用获取参数const并将其复制到函数中时(如上所述),最好按值获取参数(因为在某些情况下,编译器可能会避免复制)。总之,这个实现是更可取的:

void Capitalize_And_Output_impl(std::string str) {
    Capitalize_And_Output_impl(str);
}

不幸的是,对于第一个重载,Capitalize_And_Output_impl对左值的调用也可以定向到这个重载。这会产生编译器抱怨的歧义。这就是为什么我们需要第二个参数。

第一个重载采用 a int,第二个采用 a long。因此,传递作为0an 的字面量int会使第一个重载优于第二个重载,但仅在出现歧义时才会如此。在其他情况下,即当第一个参数是右值或const左值时,第一个重载不能使用,而第二个重载可以在文字0被提升为之后使用long

最后两句。(1)如果你想避免递归调用Capitalize_And_Output(我想这只是一个品味问题),那么你可以使用与 Simple 的解决方案相同的技巧(通过unpack)和(2)我不认为需要::toupper像 Simple 的解决方案一样传递 lambda 包装。

于 2013-10-28T17:53:03.917 回答
2

为此,您不需要类型特征:

char safer_toupper(unsigned char const c)
{
    return static_cast<char>(std::toupper(c));
}

void Capitalize_And_Output_Impl(std::string& str)
{
    auto const first = str.begin();
    std::transform(first, str.end(), first, safer_toupper);
    std::cout << str;
}

void Capitalize_And_Output_Impl(std::string const& str)
{
    std::transform(str.begin(), str.end(),
                   std::ostreambuf_iterator<char>(std::cout),
                   safer_toupper);
}

void Capitalize_And_Output_Impl(char const* const str)
{
    std::transform(str, str + std::strlen(str),
                   std::ostreambuf_iterator<char>(std::cout),
                   safer_toupper);
}

template<typename... Strings>
void Capitalize_And_Output(Strings&&... rest)
{
    int const unpack[]{0, (Capitalize_And_Output_Impl(rest),
                           std::cout << ' ', 0)...};
    static_cast<void>(unpack);
    std::cout << std::endl;
}
于 2013-10-28T16:13:05.843 回答
1

这个版本没有不必要的参数复制,没有引入不必要的临时字符串,并避免调用strlen()在编译时长度已知的文字字符串。

#include <algorithm>
#include <cctype>
#include <cstring>
#include <iostream>
#include <iterator>
#include <string>
#include <type_traits>
#include <vector>

template<typename I> void CapitalizeAndOutputImpl(I first, I last) {
    std::string t;
    std::transform(first, last, std::back_inserter(t), std::toupper);
    std::cout << t << " ";
}

template<typename T>
struct CapitalizeAndOutputHelper {
    void operator()(const T& s) {
        CapitalizeAndOutputImpl(std::begin(s), std::end(s));
    }
};

template<typename T>
struct CapitalizeAndOutputHelper<T*> {
    void operator()(const T* s) {
        CapitalizeAndOutputImpl(s, s + std::strlen(s));
    }
};

template<typename T> void CapitalizeAndOutput(T&& s) {
    CapitalizeAndOutputHelper<std::remove_reference<T>::type>()(s);
    std::cout << std::endl;
}

template<typename First, typename... Rest> void CapitalizeAndOutput(First&& first, Rest&&... rest) {
    CapitalizeAndOutputHelper<std::remove_reference<First>::type>()(first);
    CapitalizeAndOutput(rest...);
}

int main() {
    std::string hello{ "string hello" };
    const std::string world{ "const string world" };
    char arrHello[] = "char[] hello";
    const char vHelloInit[] = "char* hello";
    std::vector<char> vHello(std::begin(vHelloInit), std::end(vHelloInit));
    const char* cworld = "const char* world";
    CapitalizeAndOutput(hello, world, arrHello, "literal world", vHello.data(), cworld);
}
于 2013-10-28T18:22:18.200 回答
0

到目前为止,最简单的是有两个重载:

void do_stuff() {}
template<class...Ts>
void do_stuff(std::string s, Ts&&... ts);

第二次使用你现有的身体。

我们得到完美的转发,然后在你的变异和输出之前,我们复制。

如果你想让变异传播出去,你可能错了。如果您坚持,@Cassio 的方法看起来不错。

于 2013-10-29T11:29:50.297 回答