使用const char *作为非类型参数的模板技巧

时间:2015-06-02 19:15:11

标签: c++ templates c++11 constexpr

我非常清楚直接传递const char*作为模板非类型参数是错误的,因为在两个不同的翻译单元中定义的两个相同的字符串文字可能具有不同的地址(尽管大多数情况下编译器使用相同的地址)。可以使用一个技巧,请参阅下面的代码:

#include <iostream>

template<const char* msg>
void display()
{
    std::cout << msg << std::endl;
}

// need to have external linkage 
// so that there are no multiple definitions
extern const char str1[] = "Test 1"; // (1)

// Why constexpr is enough? Does it have external linkage?
constexpr char str2[] = "Test 2";    // (2)

// Why doesn't this work? 
extern const char* str3 = "Test 3";  // (3) doesn't work

// using C_PTR_CHAR = const char* const;   // (4) doesn't work either
extern constexpr C_PTR_CHAR str4 = "Test 4"; 

int main()
{
    display<str1>();    // (1')
    display<str2>();    // (2')
    // display<str3>(); // (3') doesn't compile 
    //display<str4>();  // (4') doesn't compile
}

基本上在(1)中我们声明和定义具有外部链接的数组,然后可以将其用作(1')中的模板参数。我非常了解这一点。但是,我不明白:

  1. 为什么constexpr版本(2)有效? constexpr有外部联系吗?如果不是,则在不同的翻译单元中定义相同的字符串文字可能导致重复的模板实例化。

  2. 为什么(3)和(4)不起作用?对我来说这似乎是完全合理的,但编译器并不这么认为:

      

    错误:'str3'不是有效的模板参数,因为'str3'是变量,而不是变量的地址

1 个答案:

答案 0 :(得分:14)

1。简短回答:它无论是否被声明为constexpr都有效,因为您正在定义一个具有静态存储持续时间的对象(不是字符串文字 - 它存储一个内容的副本),其地址是一个常量表达式。关于链接,str2有内部链接,但没关系 - 它的地址可以用作非类型模板参数。

答案很长:

在C ++ 11和14中,[14.3.2p1]说明如下:

  

template-argument ,用于非类型的非模板模板参数   应为以下之一:
  [...]

     
      
  • 一个常量表达式(5.19),用于指定具有静态存储持续时间和外部或内部的完整对象的地址   连接或具有外部或内部联系的功能,包括   函数模板和函数 template-id 但不包括非静态函数   类成员,表达(忽略括号)为& id-expression ,   其中 id-expression 是对象或函数的名称,除外   如果名称引用函数或数组,则&可以省略   如果相应的 template-parameter 是a,则应省略   参考;
  •   
     

[...]

因此,您可以使用具有静态存储持续时间的对象的地址,但该对象必须由具有链接(内部或外部)的名称标识,并且您表达该地址的方式受到限制。 (字符串文字不是名称,也没有联系。)

简而言之,即使char str1[] = "Test 1";也可以。 static char str1[] = "Test 1";也很好; GCC 5.1.0拒绝它,但我认为这是一个错误; Clang 3.6.0接受了它。

关于str2的链接,C ++ 11和14 [3.5p3]说:

  

具有命名空间作用域(3.3.6)的名称具有内部链接if   它是
的名字   [...]

     
      
  • 显式声明为constconstexpr的非易失性变量,既未明确声明extern也未明确声明extern   宣布有外部联系;
  •   
     

[...]

由于DR 1686,N4431略有改变:

  
      
  • 非易失性const限定类型的变量,既未明确声明constexpr也未声明具有外部   键;
  •   

反映了str3暗示对象的const限定的事实。

2。简答:对于C ++ 11和14,请参见上文;对于草案C ++ 1z,constexpr不是常量表达式,因为指针本身不是str4,它也是字符串文字的地址。 typeid是常量,但仍然是字符串文字的地址。

答案很长:

在当前的工作草案N4431中,放宽了对非类型模板参数的约束。 [14.3.2p1]现在说:

  

非类型模板参数 template-argument 应为   转换常数表达式(5.20)的类型   模板参数。对于非类型模板参数的引用或   指针类型,常量表达式的值不得参考   (或者对于指针类型,不应该是地址):

     
      
  • 子对象(1.8),
  •   
  • 临时对象(12.2),
  •   
  • 字符串文字(2.13.5),
  •   
  • __func__表达式(5.2.8)或
  • 的结果   
  • 预定义的constexpr变量(8.4.1)。
  •   

这些都是限制。 转换的常量表达式部分非常重要;完整定义是 long ,但与我们的案例相关的一部分是具有静态存储持续时间的对象的地址就是这样的表达式。

同样相关的是,根据[5.20p2.7],应用于左值的左值转换

  

非易失性glvalue,它引用定义的非易失性对象   使用constexpr,或者引用此类的非可变子对象   对象

也满足作为常数表达式的条件。这允许我们使用一些const指针变量作为非类型模板参数。 (请注意,仅仅声明变量constexpr const char* str3 = str1;是不够的,因为它可以使用非常量表达式进行初始化。)

所以像{{1}}这样的东西很好。它在C ++ 1z模式下被Clang 3.6.0接受(并在C ++ 14模式下被拒绝); GCC 5.1.0仍然拒绝它 - 它看起来还没有实现更新的规则。

仍然,字符串文字有什么问题?这就是问题所在(N4431 [2.13.5p16]):

  

评估 string-literal 会产生一个字符串文字对象   静态存储持续时间,从给定字符初始化为   如上所述。是否所有字符串文字都是不同的(即,   存储在非重叠对象中)以及是否连续   对 string-literal 的评估会产生相同或不同的对象   没有具体说明。

允许实现使用字符串文字做很多事情:混合,匹配,使它们重叠(完全或部分),从同一个翻译单元制作7个副本 - 无论如何。这使得字符串文字的地址无法用作非类型模板参数。