Связывание C++ и специализации шаблонов

Я изучаю поведение компоновщика C++ в отношении специализаций шаблонов. Я использую Microsoft Visual C++ 2010 для этих экспериментов. Я не знаю, такое ли поведение с другими наборами инструментов (например, gcc).

Вот первый фрагмент кода:

// bar.cpp

template <typename T> int foo() { return 1; }
int bar() { return foo<double>(); }

// main.cpp

template <typename T> int foo() { return 1; }
template <> int foo<double>() { return 2; }

int bar();

int main()
{
    const int x = bar();
    const int y = foo<double>();  // doesn't link
}

Как и следовало ожидать, этот код не связан, потому что foo<double>() имеет несколько определений, поскольку он создается один раз в bar.cpp и один раз в main.cpp (через специализацию). Затем мы ожидаем, что если эта программа свяжется, что bar() и main() будут использовать разные экземпляры foo(), так что в конце мы получим x == 1 и y == 2.

Давайте исправим ошибку ссылки, объявив специализацию foo<double>() как static:

// bar.cpp

template <typename T> int foo() { return 1; }
int bar() { return foo<double>(); }

// main.cpp

template <typename T> int foo() { return 1; }
template <> static int foo<double>() { return 2; }  // note: static

int bar();

int main()
{
    const int x = bar();          // x == 1
    const int y = foo<double>();  // y == 2
}

Теперь у нас есть x == 1 и y == 2, как мы и ожидали. (Примечание: мы должны использовать здесь ключевое слово static: анонимное пространство имен не подойдет, поскольку мы не можем специализировать шаблонную функцию в пространстве имен, отличном от его объявления.)

Теперь использование ключевого слова static довольно неинтуитивно. Как правило, специализация foo<double>() находится где-то в заголовочном файле и поэтому помечается как встроенная, как в следующем фрагменте:

// bar.cpp

template <typename T> int foo() { return 1; }
int bar() { return foo<double>(); }

// main.cpp

template <typename T> int foo() { return 1; }
template <> inline int foo<double>() { return 2; }  // note: inline

int bar();

int main()
{
    const int x = bar();          // x == 2
    const int y = foo<double>();  // y == 2
}

Теперь этот код правильно компонуется, и когда мы его запускаем, мы получаем x == 2 и y == 2. Этот момент меня удивляет: почему существует единственное определение foo<double>()? Что означает inline в этом коде?

Последний фрагмент:

// bar.cpp

template <typename T> int foo() { return 1; }
int bar() { return foo<double>(); }

// main.cpp

template <typename T> int foo() { return 1; }
template <> inline int foo<double>() { return 2; }  // note: inline

int bar();

int main()
{
    const int x = bar();             // x == 1
    // const int y = foo<double>();  // note: commented out
}

Этот случай на самом деле неудивителен: специализация foo<double>() больше не реализуется в main.cpp (хотя объявление все еще там), поэтому остается только одна конкретизация в bar.cpp. .


person François Beaune    schedule 25.08.2011    source источник
comment
В вашем третьем абзаце вызов в bar.cpp является не неявной специализацией, а скорее созданием экземпляра, поскольку вы только вызываете foo<double>() (вы ничего не специализируете). Кроме того, в следующем предложении явная специализация, вероятно, должна быть просто специализацией или полной специализацией, чтобы избежать путаницы с похожим по звучанию явным воплощением (которого у вас нет).   -  person Kerrek SB    schedule 25.08.2011
comment
Я думаю, что если вы inline используете функцию, вы обещаете, что она будет иметь одинаковое определение во всех единицах перевода. Если это правда, то вы подвергаетесь неопределенному поведению.   -  person Kerrek SB    schedule 25.08.2011
comment
@Kerrek На самом деле это неправда. Встроенные функции могут иметь разные реализации в разных единицах перевода. Они являются исключением из правила одного определения.   -  person Šimon Tóth    schedule 25.08.2011
comment
@Let_Me_Be: Нет, это не так. Каждое определение таких вещей должно состоять из одной и той же последовательности токенов, иначе у вас будет неопределенное поведение. Цепочка инструментов не обязана давать какую-либо диагностику, когда какой-либо аспект правила одного определения нарушается.   -  person David Hammen    schedule 25.08.2011
comment
@David Встроенные функции с одинаковым именем, но разными определениями считаются отдельными объектами и не нарушают ODR.   -  person Šimon Tóth    schedule 25.08.2011
comment
@Let_Me_Be: я думаю, что специализация шаблона имеет приоритет над inline или static здесь. Параграф о специализации шаблонов не содержит специальных положений для них.   -  person Matthieu M.    schedule 25.08.2011
comment
@Matthieu Да, это дело сломано по другой причине. Я просто отвечал Керреку.   -  person Šimon Tóth    schedule 25.08.2011
comment
@Kerrek Спасибо, я исправил формулировку, как вы предложили в своем первом комментарии.   -  person François Beaune    schedule 25.08.2011
comment
@Let_Me_Be: встроенные функции не освобождаются от правила одного определения. Шаблоны функций являются, но не (полными) специализациями шаблонов функций.   -  person Maxim Egorushkin    schedule 25.08.2011
comment
@Maxim Да, ты прав, я не знал, что в С++ нет extern inline и static inline :-/   -  person Šimon Tóth    schedule 25.08.2011


Ответы (1)


Вы на самом деле нарушаете правило С++ здесь (выделено мной):

14.7.3 [temp.expl.spec]:

6/ Если шаблон, шаблон члена или член шаблона класса явно специализированы, то эта специализация должна быть объявлена ​​до первого использования этой специализации, которое вызовет неявное создание экземпляра, в каждая единица перевода, в которой происходит такое использование; диагностика не требуется. Если программа не предоставляет определения для явной специализации и либо специализация используется таким образом, что может привести к неявному созданию экземпляра, либо член является виртуальной функцией-членом, программа неправильно сформирована, нет. требуется диагностика. Неявная реализация никогда не создается для явной специализации, которая объявлена, но не определена. [ Пример:

class String { };

template<class T> class Array { /* ... */ };
template<class T> void sort(Array<T>& v) { /* ... */ }

void f(Array<String>& v) {
  sort(v); // use primary template
  // sort(Array<T>&), T is String
}

template<> void sort<String>(Array<String>& v); // error: specialization
                                                // after use of primary template

template<> void sort<>(Array<char*>& v); // OK: sort<char*> not yet used

template<class T> struct A {
  enum E : T;
  enum class S : T;
};

template<> enum A<int>::E : int { eint }; // OK
template<> enum class A<int>::S : int { sint }; // OK
template<class T> enum A<T>::E : T { eT };
template<class T> enum class A<T>::S : T { sT };
template<> enum A<char>::E : int { echar }; // ill-formed,
                                            // A<char>::E was instantiated
                                            // when A<char> was instantiated
template<> enum class A<char>::S : int { schar }; // OK

конец примера ]

person Matthieu M.    schedule 25.08.2011