Использование строковых литералов в качестве аргумента шаблона.

Данная проблема заинтересовала меня благодаря одному из вопросов на сайте ru.stackoverflow.com. При написании статьи я руководствовался стандартом C++11 и использовал компилятор GCC 6.2.0.

Рассмотрим возможность применения строковых литералов (string literals) в качестве аргументов шаблона при инстанцировании. Например:

#include <iostream>

template<const char* s>
class A {
public:
    void foo() {
        std::cout << s << "\n";
    }
};

int main() {
    A<"Hello, world!"> a;
    a.foo();
    return 0;
}

Данный код не будет скомпилирован с сообщением об ошибке:

main.cpp: In function ‘int main()’:
main.cpp:12:22: error: ‘"Hello, world!"’ is not a valid template argument for type ‘const char*’ because string literals can never be used in this context
     A<"Hello, world!"> a;

Давайте попробуем разобраться, почему строковый литерал никогда не может быть использован в данном контексте и какие есть варианты решения этой проблемы.

Начнём с рассмотрения самого шаблона:

template<const char* s> class A {...}

Это определение шаблона класса с использованием бестипового параметра (non-type template parameter). При инстанцировании шаблона на бестиповый аргумент действует ряд ограничений:

  • For integral and arithmetic types, the template argument provided during instantiation must be a converted constant expression of the template parameter's type (so certain implicit conversion applies).
  • For pointers to objects, the template arguments have to designate the address of an object with static storage duration and a linkage (either internal or external), or a constant expression that evaluates to the appropriate null pointer or std::nullptr_t value.
  • For pointers to functions, the valid arguments are pointers to functions with linkage (or constant expressions that evaluate to null pointer values).
  • For lvalue reference parameters, the argument provided at instantiation cannot be a temporary, an unnamed lvalue, or a named lvalue with no linkage (in other words, the argument must have linkage).
  • For pointers to members, the argument has to be a pointer to member expressed as &Class::Member or a constant expression that evaluates to null pointer or std::nullptr_t value. 

Обратите внимание на второй пункт: для указателей на объекты аргумент шаблона должен указывать на адрес объекта со статической продолжительностью хранения и линковой (либо внешней, либо внутренней), или быть константным выражением, результатом которого является соответствующий null указатель или std::nullptr_t значение. Обратимся к документации на строковые литералы, где есть следующие записи:

String literals have static storage duration, and thus exist in memory for the life of the program.

...

The compiler is allowed, but not required, to combine storage for equal or overlapping string literals. That means that identical string literals may or may not compare equal when compared by pointer.

Как видим, строковые литералы имеют статическую продолжительность хранения, но не имеют линковки. Это означает, что в пределах даже одной единицы трансляции адреса одинаковых строковых литералов могут различаться (а могут и совпадать, всё зависит от конкретной реализации компилятора), и использование в различных участках кода записи вида A<"Hello, world!"> может приводить к инстанцированию разных шаблонов. Поэтому этот случай оговорен в стандарте и явно запрещён:

In particular, this implies that string literals, addresses of array elements, and addresses of non-static members cannot be used as template arguments to instantiate templates whose corresponding non-type template parameters are pointers to objects.

Разобравшись с причиной ошибки, попробуем её устранить:

#include <iostream>

template<const char* s>
class A {
public:
    void foo() {
        if (s) {
            std::cout << s << "\n";
        } else {
            std::cout << "Hello from nullptr!" << "\n";
        }
    }
};

// Создаем дополнительную переменную со статической
// продолжительностью хранения и внешней линковкой.
extern const char p[];
const char p[] = "Hello from extern!";

// Либо используем переменную времени компиляции
// с внутренней линковкой.
constexpr static const char s[] = "Hello from constexpr!";

// Либо используем константное выражение, 
// результатом которого является nullptr.
constexpr static const char* c = nullptr;

int main() {
    A<p> a;
    a.foo();

    A<s> b;
    b.foo();

    A<c> c;
    c.foo();

    return 0;
}

В консоль будет выведено:

Hello from extern!
Hello from constexpr!
Hello from nullptr!

 

Теги: