不允许将字符串文本作为非类型模板参数

String literals not allowed as non type template parameters

本文关键字:类型 参数 字符串 文本 不允许      更新时间:2023-10-16

以下引文来自Addison Wesley的C++ Templates。有人可以帮我用简单的英语/外行术语理解它的要点吗?

由于字符串文本是具有

内部链接的对象(两个具有相同值但不同模块中的字符串文本是不同的对象),因此也不能将它们用作模板参数:

编译器最终对称为翻译单元(非正式地称为源文件)的内容进行操作。在这些翻译单元中,您可以识别不同的实体:对象、函数等。链接器的工作是将这些单元连接在一起,该过程的一部分是合并标识。

标识符具有链接内部链接意味着在该翻译单元中命名的实体仅对该翻译单元可见,而外部链接意味着该实体对其他单元可见。

当一个实体被标记为static时,它被赋予内部链接。因此,给定这两个翻译单元:

// a.cpp
static void foo() { /* in a */ } 
// b.cpp
static void foo() { /* in a */ } 

这些foo中的每一个都是指一个实体(在本例中是一个函数),该实体仅对各自的翻译单元可见;也就是说,每个翻译单元都有自己的foo

那么,这里有一个问题:字符串文字与static const char[..]的类型相同。那是:

// str.cpp
#include <iostream>
// this code:
void bar()
{
    std::cout << "abc" << std::endl;
}
// is conceptually equivalent to:
static const char[4] __literal0 = {'a', 'b', 'c', 0};
void bar()
{
    std::cout << __literal0 << std::endl;
}

如您所见,文字的值是该翻译单元的内部值。因此,例如,如果您在多个翻译单元中使用"abc",它们最终都是不同的实体。

总的来说,这意味着这在概念上毫无意义:

template <const char* String>
struct baz {};
typedef baz<"abc"> incoherent;

因为每个翻译单元"abc"都不同。每个翻译单元将被赋予不同的类,因为每个"abc"都是不同的实体,即使它们提供了"相同"的参数。

在语言层面上,这是通过说模板非类型参数可以指向具有外部链接的实体的指针来实现的;也就是说,跨翻译单元引用同一实体的事物。

所以这很好:

// good.hpp
extern const char* my_string;
// good.cpp
const char* my_string = "any string";
// anything.cpp
typedef baz<my_string> coherent; // okay; all instantiations use the same entity

†并非所有标识符都有链接;有些标识符没有链接,例如函数参数。

‡ 优化编译器将在同一地址存储相同的文字,以节省空间;但这是实现细节的质量,而不是保证。

这意味着你不能这样做...

#include <iostream>
template <const char* P>
void f() { std::cout << P << 'n'; }
int main()
{
    f<"hello there">();
}

。因为"hello there"不能 100% 保证解析为可用于实例化模板一次的单个整数值(尽管大多数好的链接器会尝试跨链接对象折叠所有用法,并使用字符串的单个副本生成一个新对象)。

但是,您可以使用外部字符数组/指针:

...
extern const char p[];
const char p[] = "hello";
...
    f<p>();
...

显然,像"foobar"这样的字符串文字不像其他文字内置类型(如int或float)。他们需要有一个地址(常量字符*)。地址实际上是编译器替换的常量值,以代替文本出现的位置。该地址指向程序内存中的某个位置,在编译时固定

因此,它必须是内部联系的。内部链接仅意味着不能跨翻译单元(编译的 cpp 文件)链接。编译器可以尝试执行此操作,但不是必需的。换句话说,内部链接意味着,如果您在不同的 cpp 文件中获取两个相同的文本字符串的地址(即它们转换为的 const char* 的值),它们通常不会相同。

您不能将它们用作模板参数,因为它们需要 strcmp() 来检查它们是否相同。如果使用 ==,则只是比较地址,当模板在不同的翻译单元中使用相同的文本字符串实例化时,地址将有所不同。

其他更简单的内置类型(如文字)也是内部链接(它们没有标识符,不能从不同的翻译单元链接在一起)。但是,它们的比较是微不足道的,因为它是按价值计算的。因此,它们可以用于模板。

如其他答案中所述,字符串文本不能用作模板参数。但是,有一种具有类似效果的解决方法,但"字符串"限制为四个字符。这是由于多字符常量,如链接中所述,这些常量可能相当不可移植,但适用于我的调试目的。

template<int32_t nFourCharName>
class NamedClass
{
    std::string GetName(void) const
    {
        // Evil code to extract the four-character name:
        const char cNamePart1 = static_cast<char>(static_cast<uint32_t>(nFourCharName >> 8*3) & 0xFF);
        const char cNamePart2 = static_cast<char>(static_cast<uint32_t>(nFourCharName >> 8*2) & 0xFF);
        const char cNamePart3 = static_cast<char>(static_cast<uint32_t>(nFourCharName >> 8*1) & 0xFF);
        const char cNamePart4 = static_cast<char>(static_cast<uint32_t>(nFourCharName       ) & 0xFF);
        std::ostringstream ossName;
        ossName << cNamePart1 << cNamePart2 << cNamePart3 << cNamePart4;
        return ossName.str();
    }
};

可用于:

NamedClass<'Greg'> greg;
NamedClass<'Fred'> fred;
std::cout << greg.GetName() << std::endl;  // "Greg"
std::cout << fred.GetName() << std::endl;  // "Fred"

正如我所说,这是一种解决方法。我不假装这是好的、干净的、可移植的代码,但其他人可能会发现它很有用。另一种解决方法可能涉及多个 char 模板参数,如本答案所示。

c++标准只允许模板使用某些类型的参数的想法是,参数应该是常量并且在编译时已知,以便生成"专用类"代码。

对于此特定情况:当您创建字符串文字时,它们的地址在链接时间之前是未知的(链接发生在编译后),因为跨不同翻译单元的两个字符串文字是两个不同的对象(正如接受的答案所解释的那样)。当编译发生时,我们不知道使用哪个字符串文字的地址从模板类生成专用类代码。

在 C++20 中,您可以使用内联外部链接做得更好:

演示

#include <cstdio>
inline const char myliteral[] = "Hello";
template <const char* Literal>
struct mystruct
{
    auto print() {
        printf(Literal);
    }
};
int main(){
    mystruct<myliteral> obj;
    
    obj.print();
}

从本质上讲,它的作用留给链接器来确定最终使用哪个定义。