使用constexpr、SFINAE和/或type_traits进行char*、char数组和字符串字面量的重载解析

3
我遇到了一个有趣的挑战,我已经尝试解决几个小时了,但是经过多次研究和尝试失败后,我发现自己问了这个问题。
我想编写3个重载函数,每个函数分别接受以下类型之一:const char*const char(&)[N]字符串字面量(例如"BOO")。 我知道字符串字面量只是一个char数组,但是请容许我解释我的方法。
下面的两个函数能够通过包装类CharPtrWrapper区分前两种类型(const char*const char(&)[N]):
#include <iostream>

class CharPtrWrapper
{
public:
    CharPtrWrapper(const char* charPtr)
        : m_charPtr(charPtr)
    {

    }

    const char * m_charPtr;
};

void processStr(CharPtrWrapper charPtrWrapper)
{
    std::cout << "From function that takes a CharPtrWrapper = " << charPtrWrapper.m_charPtr << '\n';
}

template<std::size_t N>
void processStr(const char (&charArr)[N])
{
    std::cout << "From function that takes a \"const char(&)[N]\" = " << charArr << '\n';
}

int main()
{
    const char* charPtr = "ABC";
    processStr(charPtr);

    const char charArr[] = {'X', 'Y', 'Z', '\0'};
    processStr(charArr);
}

输出:

From function that takes a CharPtrWrapper = ABC
From function that takes a "const char(&)[N]" = XYZ

现在,如果我使用字符串字面量调用processStr(例如processStr("BOO")),将调用接受const char(&)[N]的版本,这是有道理的,因为字符串字面量只是一个char数组。
这就是问题的关键所在。我一直没有能够编写出一个能够区分char数组和字符串字面量的函数。我想可能会有一个方案,就是编写一个接收rvalue引用的版本:
template<std::size_t N>
void processStr(const char (&&charArr)[N])
{
    std::cout << "From function that takes a \"const char(&&)[N]\" = " << charArr << '\n';
}

但事实上,字符串字面值是左值。我也尝试了使用std :: enable_if和std :: is_array的不同版本,但仍然没有得到我想要的结果。
因此,我的问题是:在现代C ++中,是否有可能区分char数组和字符串字面值?

1
为什么?为什么?你需要这个吗? - Walter
为什么需要区分这两个?我甚至不认为这是可能的,但是再次强调,字符串字面值和 char * 之间有什么重要的区别? - user10957435
请查看我昨天提出的这个问题,以了解为什么我需要这样做。我想改进一个开源库。 - user3266738
4
我还没有能够编写一个函数,可以区分字符数组和字符串字面值。字符串字面值就是字符数组,它们的类型完全相同,所以你无法区分它们。甚至它是一个左值,所以你也不能根据左值/右值来区分它们。 - NathanOliver
你可能可以使用C++20中的consteval来实现你的实际目标,但我认为仅使用C++17是没有办法的。 - Miles Budnek
编译器更喜欢非模板重载,而不是可能更适合的模板重载。虽然我不知道原因,但这已经是自C++11之前的行为了。 - Nathan Ernst
2个回答

2
最初的回答:
根据 [expr.prim.id.unqual]
[...] 表达式的类型是标识符的类型。结果是由标识符所表示的实体。如果实体是函数、变量或数据成员,表达式是左值;否则,它是右值。如果标识符指定了一个位域,则它是位域([dcl.struct.bind])。
因此,对于一个声明:
const char arr[] = "foo";

表达式arr是类型为const char[4]的lvalue。
根据[lex.string]/8
普通字符串字面量和UTF-8字符串字面量也被称为窄字符串字面量。 窄字符串字面量的类型为“长度为nconst char数组”,其中n如下所定义,并具有静态存储期。
根据[expr.prim.literal]
字面值是主要表达式。它的类型取决于其形式。 字符串字面量是lvalue;所有其他字面值都是prvalue。
因此,表达式"foo"是类型为const char[4]的lvalue。
结论:函数无法区分(const)char数组和字符串字面量。

谢谢您的回答,@L. F.!我很失望这是不可能的,因为我觉得如果可以的话,它会非常有益。 - user3266738

0
我也一直在研究如何优化编译时字符串字面量。虽然我认为在C++中没有标准的方法来做到这一点,但我发现在GCC、Clang和Intel编译器中,至少可以使用__builtin_constant_p内建函数来确定值是否是常量。
请注意,这个内建函数不能可靠地用于指针或引用本身(因为指针的常量折叠在Clang中目前不起作用,并且即使是常量指针也不能保证常量内容),所以诀窍是手动遍历字符串并验证每个字符是否是常量。
#include <cstdlib>

constexpr bool is_const_str(const char *s) {
    for (; *s; s++) {
        if (!__builtin_constant_p(*s)) return false;
    }
    return true;
}

bool test_dynamic(const char *s) {
    return is_const_str(s);
}

bool test_const() {
    return is_const_str("Hello, world!");
}

这是一个Godbolt链接,演示了在GCC和Clang中的工作情况。

https://godbolt.org/z/ch89Wd8Yb

在这两个程序集中,你可以看到test_const被折叠成1(也就是true),而test_dynamic被折叠成对第一个字符的简单检查(也就是,如果字符串为空,则被视为常量,否则不是)。
test_dynamic(char const*):                    # @test_dynamic(char const*)
        cmp     byte ptr [rdi], 0
        sete    al
        ret
test_const():                        # @test_const()
        mov     al, 1
        ret

我找不到MSVC的替代方案,但至少可以很容易地添加一个针对MSVC的回退,始终返回false,这样代码仍然有效且可以编译,只是无法从所述的优化中受益。
还值得注意的是,与常规的constexpr函数不同,这个内置函数依赖于优化 - 也就是说,只有在编译器能够证明输入字符都是常量时,它才会返回true,而这只能在内联之后才能实现,而在调试模式下不会发生内联。只要你只用它来进行自己的优化,应该没问题,但如果用于其他用途,就不能依赖它返回可预测的结果。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接