键入特征以区分常量字符 [N] 和 std::string

Type traits to distinguish between a const char[N] and a std::string?

本文关键字:std string 字符 特征 常量      更新时间:2023-10-16

如何编写一个可变参数模板,该模板将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::condition?也许我需要通过在模板中创建一个 auto&&& 类型的局部变量来解决它? 到目前为止,我在尝试过的各种事情上没有任何成功。

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

(1) 编译此测试用例失败

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

因为earth是一个const std::string,没有过载可以接听这个电话。(试试吧!

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

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(除了第二个重载),所以它不需要提前知道字符串的大小。因此,对于char*,无需调用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 解决方案的评论中要求的那样)。

第二个 oveload 旨在取其他所有内容,即右值和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的调用也可以定向到此重载。这会产生编译器抱怨的歧义。这就是为什么我们需要第二个论点。

第一次重载需要int,第二次需要long。因此,传递文字0,这是一个int,使得第一个重载比第二个重载更可取,但只有当出现歧义时。在其他情况下,即,当第一个参数是右值或const左值时,第一个重载不能使用,而第二个可以在文字0提升为 long 之后使用。

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

您不需要为此提供类型特征:

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;
}

此版本不做不必要的参数复制,不引入不必要的临时字符串,并避免对编译时已知长度的文本字符串调用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);
}

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

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

并第二次使用您现有的身体。

我们

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

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