Template tricks with const char* as a non-type par

2019-02-01 08:29发布

问题:

I am very well aware that passing directly a const char* as a template non-type parameter is erroneous, since two identical string literals defined in two different translation units may have different addresses (although most of the time the compilers use the same address). There is a trick one may use, see code below:

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

Basically in (1) we declare and define an array with external linkage, which can then be used as a template parameter in (1'). I understand this very well. However, I don't understand:

  1. Why the constexpr version (2) works? Do constexpr have external linkage? If not, then defining the same string literal in a different translation unit may lead with duplicate template instantiation.

  2. Why (3) and (4) don't work? It seems perfectly reasonable for me, but the compiler doesn't believe so:

    error: 'str3' is not a valid template argument because 'str3' is a variable, not the address of a variable

回答1:

1. Short answer: It works irrespective of it being declared constexpr, because you're defining an object with static storage duration (that is not a string literal - it stores a copy of the contents of one), and its address is a constant expression. Regarding linkage, str2 has internal linkage, but that's fine - its address can be used as a non-type template argument.

Long answer:

In C++11 and 14, [14.3.2p1] says the following:

A template-argument for a non-type, non-template template-parameter shall be one of:
[...]

  • a constant expression (5.19) that designates the address of a complete object with static storage duration and external or internal linkage or a function with external or internal linkage, including function templates and function template-ids but excluding non-static class members, expressed (ignoring parentheses) as & id-expression, where the id-expression is the name of an object or function, except that the & may be omitted if the name refers to a function or array and shall be omitted if the corresponding template-parameter is a reference;

[...]

So, you can use the address of an object with static storage duration, but the object has to be identified by a name with linkage (internal or external), and the way you're expressing that address is restricted. (String literals are not names and don't have linkage.)

In short, even char str1[] = "Test 1"; works. static char str1[] = "Test 1"; is fine as well; GCC 5.1.0 rejects it, but I think that's a bug; Clang 3.6.0 accepts it.


About str2's linkage, C++11 and 14 [3.5p3] says:

A name having namespace scope (3.3.6) has internal linkage if it is the name of
[...]

  • a non-volatile variable that is explicitly declared const or constexpr and neither explicitly declared extern nor previously declared to have external linkage;

[...]

N4431 has changed that slightly, as a result of DR 1686, to:

  • a variable of non-volatile const-qualified type that is neither explicitly declared extern nor previously declared to have external linkage;

reflecting the fact that constexpr implies const-qualification for objects.


2. Short answer: For C++11 and 14, see above; for draft C++1z, str3 is not a constant expression, as the pointer itself is not constexpr, and it's also the address of a string literal. str4 is constant, but still an address of a string literal.

Long answer:

In the current working draft, N4431, the constraints on non-type template arguments have been relaxed. [14.3.2p1] now says:

A template-argument for a non-type template-parameter shall be a converted constant expression (5.20) of the type of the template-parameter. For a non-type template-parameter of reference or pointer type, the value of the constant expression shall not refer to (or for a pointer type, shall not be the address of):

  • a subobject (1.8),
  • a temporary object (12.2),
  • a string literal (2.13.5),
  • the result of a typeid expression (5.2.8), or
  • a predefined __func__ variable (8.4.1).

And those are all the restrictions. The converted constant expression part is pretty important; the full definition is long, but one part relevant to our case is that the address of an object with static storage duration is such an expression.

Also relevant is that, according to [5.20p2.7], an lvalue-to-rvalue conversion applied to

a non-volatile glvalue that refers to a non-volatile object defined with constexpr, or that refers to a non-mutable sub-object of such an object

also satisfies the conditions for being a constant expression. This allows us to use some constexpr pointer variables as non-type template arguments. (Note that simply declaring a variable const is not enough, as it can be initialized with a non-constant expression.)

So, something like constexpr const char* str3 = str1; is fine. It's accepted by Clang 3.6.0 in C++1z mode (and rejected in C++14 mode); GCC 5.1.0 still rejects it - it looks like it hasn't implemented the updated rules yet.


Still, what's wrong with string literals? Here's the problem (N4431 [2.13.5p16]):

Evaluating a string-literal results in a string literal object with static storage duration, initialized from the given characters as specified above. Whether all string literals are distinct (that is, are stored in nonoverlapping objects) and whether successive evaluations of a string-literal yield the same or a different object is unspecified.

An implementation is allowed to do lots of things with string literals: mix, match, make them overlap (entirely or partially), make 7 copies from the same translation unit - whatever. That makes the address of a string literal unusable as a non-type template argument.