Почему явное создание экземпляра шаблона не нарушает ODR?

Этот вопрос возник в контексте этого ответа.

Как и следовало ожидать, эта единица перевода не компилируется:

template <int Num> int getNum() { return Num; }
template int getNum<0>();
template int getNum<0>();  // error: duplicate explicit instantiation of 'getNum<0>'
int main() { getNum<0>(); return 0; }

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

// decl.h
template <int Num> int getNum() { return Num; }

// a.cc
#include <decl.h>
template int getNum<0>();

// b.cc
#include <decl.h>
template int getNum<0>();
int main() { getNum<0>(); return 0; }

Я не ожидал этого. Я предполагал, что несколько явных экземпляров шаблона с одними и теми же параметрами нарушат ODR, но, похоже, это не так. Однако это не удается:

// decl.h
template <int Num> int getNum();

// a.cc
#include "decl.h"
template <> int getNum<0>() { return 0; }

// b.cc
#include "decl.h"
template <> int getNum<0>() { return 0; }
int main() { getNum<0>(); return 0; }

Пользователь Oliv услужливо указал мне на этот соответствующий абзац в стандарте, но я все еще немного сбит с толку, поэтому я надеялся, что кто-нибудь сможет объяснить более простыми словами логику, стоящую за этим (например, что должно или не следует рассматривать как нарушение ODR и почему мое ожидание было неверным).

РЕДАКТИРОВАТЬ:

В качестве еще одного примера, вот программа, разделенная на две части, которая компилируется правильно, но дает, возможно, удивительные результаты:

// a.cc
template <int Num> int getNum() { return Num + 1; }
template int getNum<0>();

// b.cc
#include <iostream>
template <int Num> int getNum() { return Num; }
template int getNum<0>();
int main() { std::cout << getNum<0>() << std::endl; return 0; }

Выход:

1

В этом случае удаление явных экземпляров шаблона приводит к 0. Я знаю, что наличие двух шаблонов с разными определениями не является распространенным вариантом использования, но я думал, что ODR был применен именно для того, чтобы избежать подобных проблем.


person jdehesa    schedule 05.10.2018    source источник
comment
Насколько я знаю, нет никакой разницы между неявным и явным созданием экземпляров, кроме невидимости первого.   -  person molbdnilo    schedule 05.10.2018
comment
Да, экземпляры шаблонов - это слабые символы, поэтому компоновщик просто выбирает любой, который ему нравится. Однако не слишком уверен в этом, поэтому не отвечаю.   -  person Hugo Maxwell    schedule 05.10.2018
comment
@molbdnilo Спасибо за комментарий. Я понимаю, что вы имеете в виду, но я не уверен, почему так должно быть. Я добавил (запутанный) случай, когда явная инициализация приводит к неожиданному результату.   -  person jdehesa    schedule 05.10.2018
comment
Каждая программа должна содержать ровно одно определение каждой невстроенной функции или переменной, которая используется в этой программе за пределами отброшенного оператора; диагностика не требуется.   -  person geza    schedule 05.10.2018
comment
@geza Верно, это означает, что если я это сделаю, программа может скомпилироваться, но, вероятно, не будет работать должным образом, верно? Но не лучше ли рассматривать явные экземпляры шаблонов как надежные символы и при необходимости не связывать их? Есть ли польза от текущего поведения? Или есть какая-то причина, по которой компиляторы не могут этого сделать?   -  person jdehesa    schedule 05.10.2018
comment
@jdehesa: да. Вы только что привели пример этого (печать 0 против 1). О сильных символах: это решит только случай, когда у вас есть два явных экземпляра. Для других случаев (один экспл. и один импл.) это не так. Возможно, отчет о недоставке существует, потому что трудно надежно определить, являются ли два (импл.) экземпляра одинаковыми (в разных версиях компилятора или даже в разных компиляторах).   -  person geza    schedule 05.10.2018
comment
сильный/слабый символ не соответствует стандарту С++. это было сделано, чтобы не заставлять компилятор проверять это, IMO, так как в общем случае может быть сложно проверить каждое нарушение ODR.   -  person Jarod42    schedule 05.10.2018
comment
Последний ответ действительно не в тему. Точка создания экземпляра может влиять только на поиск зависимого имени (то есть, если вы выполняете вызов, который будет включать ADL и когда аргумент зависит от параметра шаблона). Все остальные имена разрешаются в точке определения шаблона. Таким образом, точка инстанцирования здесь не влияет на ODR. Для чтения: eel.is/c++draft/temp.dep. рез#1   -  person Oliv    schedule 06.10.2018


Ответы (2)


Эврика! Я наконец попадаю на соответствующий абзац, [temp.spec]/5

Для данного шаблона и заданного набора аргументов шаблона

  • (5.1) явное определение экземпляра должно появляться в программе не более одного раза,

  • (5.2) явная специализация должна быть определена в программе не более одного раза, как указано в [basic.def.odr], и

  • (5.3) как явное воплощение, так и объявление явной специализации не должны появляться в программе, если только явное воплощение не следует за объявлением явной специализации.

Реализация не требуется для диагностики нарушения этого правила.

Таким образом, явное определение создания экземпляра шаблона (не неявное создание экземпляра) может привести к нарушению ODR, диагностика не требуется (и, по крайней мере, gcc и clang - наборы инструментов ld не производят диагностику)

person Oliv    schedule 05.10.2018
comment
Отличная работа, которая действительно делает это совершенно ясным. Я полагаю, что компилятору сложнее, чем кажется, диагностировать эту ошибку. Кстати, я думаю, это означает, что ошибка компиляции для третьего примера в вопросе (со специализациями шаблона) на самом деле не гарантируется стандартом? - person jdehesa; 05.10.2018
comment
@jdehesa В третьем случае вы объявляете явный шаблон специализации, это рассматривается в другом абзаце temp.expl.spec. (Я только что прочитал это), =› диагностика не требуется - person Oliv; 05.10.2018
comment
@jdehesa Но хорошие новости, если вы компилируете с помощью gcc или clang, символ в объектном файле неслабый! Таким образом, вы всегда будете получать ошибку времени ссылки. - person Oliv; 05.10.2018
comment
О, понятно, спасибо, я думал, что это было описано в 5.2 в абзаце, который вы разместили, и что абзац, на который вы ссылались, был о порядке объявления-использования специализации. Во всяком случае, NDR в любом случае! Спасибо за проверку с помощью gcc и clang, я тестировал с MSVC, и, похоже, он делает то же самое. - person jdehesa; 05.10.2018
comment
@jdehesa Действительно, второе предложение последнего абзаца избыточно со вторым пунктом первого абзаца. - person Oliv; 05.10.2018

Как явная специализация, так и явное определение экземпляра будут нарушать ODR в зависимости от контекста, в котором они используются, и значения объектов, которые они генерируют.

Ниже объясняется первый и третий случай и почему они нарушают ODR с отчетом о недоставке [temp.spec]/5.

Для данного шаблона и заданного набора аргументов шаблона

  • (5.1) явное определение экземпляра должно появляться в программе не более одного раза,

  • (5.2) явная специализация должна быть определена в программе не более одного раза (согласно 6.2), [...]

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

с [temp.point]/6

Определение явного воплощения — это точка воплощения для специализации или специализаций, заданных явным воплощением.

и [temp.point]/8

[...] Если две разные точки инстанцирования придают специализации шаблона разные значения в соответствии с правилом одного определения (6.2), программа плохо сформирована, диагностика не требуется.

во втором случае ODR не нарушается, так как смысл конкретизации в этих TU одинаков.

// decl.h
template <int Num> int getNum() { return Num; }

// a.cc
#include <decl.h>
template int getNum<0>();

// b.cc
#include <decl.h>
template int getNum<0>();
int main() { getNum<0>(); return 0; }

Но последнее точно не допустимо (нарушение ODR NDR), потому что даже шаблоны функций имеют одинаковые сигнатуры, экземпляры из них будут иметь разное значение. Вы не можете передать результат, который вы получили, стандарт не гарантирует поведение, когда происходит это нарушение.

// a.cc
template <int Num> int getNum() { return Num + 1; }
template int getNum<0>();

// b.cc
#include <iostream>
template <int Num> int getNum() { return Num; }
template int getNum<0>();
int main() { std::cout << getNum<0>() << std::endl; return 0; }
person Jans    schedule 05.10.2018
comment
Спасибо за подробное объяснение, чтение стандарта может заставить меня почувствовать себя Граучо Марксом, но это полезно. Вы объясняете, что первый и третий примеры нарушают ODR с NDR, хотя именно в этих двух я получаю ошибки компиляции. Вы имеете в виду, что эти ошибки компиляции не гарантируются стандартом, верно? - person jdehesa; 05.10.2018
comment
в том же разделе [temp.spec]/5 говорится, что реализация не требуется для диагностики, что заставляет меня думать, что нет, они не обязаны, но тогда эти случаи ИМО легко распознать, почему бы не диагностировать их? - person Jans; 05.10.2018
comment
Афф... Второй случай недействителен, потому что нарушает temp.spec/5.1, понятнее некуда. В последнем случае нарушение ODR происходит из-за разных определений шаблонов, см. [basic.def.odr]/12, даже без какой-либо реализации шаблона! - person Oliv; 05.10.2018
comment
И последнее, но не менее важное: точка создания экземпляра может изменить определение функции шаблона только из-за поиска зависимого имени. Ни в каких определениях шаблона getNum нет зависимого поиска имени. Так что ваш ответ действительно не в тему. Извиняюсь! - person Oliv; 06.10.2018
comment
@Oliv, для второго случая (5.1) относится к появлению не более одного раза (чего нет, NDR), но мне не ясно, что прямо указано, что они представляют собой нарушение ODR, потому что [basic.def. odr]/12 относятся к специализации шаблона, для которой не указаны некоторые параметры шаблона (которыми они и не являются). - person Jans; 06.10.2018
comment
Что касается последнего случая, прочитайте еще раз [basic.def.odr]/12, 12.7 и [temp]/10, и вы увидите, что несколько определений шаблона функции не нарушают ODR, если они составлены. той же последовательности токенов (которой они не делают), и вы правы, это нарушение происходит даже без инстанцирования, я этого не отрицал. - person Jans; 06.10.2018
comment
@Jans Действительно (5.1) не относится к basic.def.odr. Но для этого требуется, чтобы в программе существовало только одно определение экземпляра шаблона. Так что это буквально, но не формально правило ODR. То же самое относится к неявному созданию экземпляров в точках создания, в стандарте никогда не говорится (что, по-видимому, предполагает ваш ответ), что вариации значения определения специализации шаблона в двух разных точках создания экземпляров представляют собой нарушение ODR. Но я согласен с вами в том, что этот вариант можно назвать нарушением УСО. - person Oliv; 06.10.2018