Обратите внимание: я не спрашиваю: «Когда создаются экземпляры шаблонов функций?» Скорее: «Когда будут видны побочные эффекты этого создания экземпляра?»
Примером побочного эффекта создания экземпляра может быть внедрение друга. Например:
// Same result on Clang, GCC, and MSVC.
template<int N>
struct FriendIndex {
friend constexpr bool Get(FriendIndex);
};
template<int N, bool val>
struct FriendInjector {
friend constexpr bool Get(FriendIndex<N>) {
return true;
}
};
template <bool val>
auto InstantSideEffect() {
(void)FriendInjector<0, val>{};
return 0;
}
template <bool val>
int DelayedSideEffect() {
(void)FriendInjector<1, val>{};
return 0;
}
int dummy0 = InstantSideEffect<true>();
// Will succeed.
static_assert(Get(FriendIndex<0>{}),"");
int dummy1 = DelayedSideEffect<true>();
// Will fail: Get(FriendIndex<1>) is not a constant expression.
static_assert(Get(FriendIndex<1>{}),"");
При создании экземпляра InstantSideEffect<true>()
функция constexpr friends Get(FriendIndex<0>)
определяется сразу после создания экземпляра и может использоваться в static_assert.
С другой стороны, при создании экземпляра DelayedSideEffect<true>()
определение Get(FriendIndex<1>)
не отображается сразу после создания экземпляра, и static_assert, использующий его, не сможет скомпилироваться.
Поскольку единственная разница между этими двумя функциями заключается в том, что InstantSideEffect()
возвращает auto
, а DelayedSideEffect()
возвращает int
, я посмотрел на стандарт, чтобы узнать, есть ли в нем какая-либо информация о влиянии auto
на то, когда эти побочные эффекты становятся видимыми.
В проекте стандарта C++14 в 7.1.6.4 [dcl.spec.auto]/12
говорится:
Вычисление типа возвращаемого значения для шаблона функции с заполнителем в объявленном типе происходит при создании экземпляра определения, даже если тело функции содержит оператор возврата с операндом, не зависящим от типа. [Примечание. Таким образом, любое использование специализации шаблона функции приведет к неявному созданию экземпляра. Любые ошибки, возникающие в результате этой реализации, не относятся к непосредственному контексту типа функции и могут привести к неправильному форматированию программы. — последнее примечание]
Это звучит так, будто auto
влияет на создание специализированных шаблонов функций, и это auto
возможно влияет на SFINAE. Однако я не вижу в стандарте ничего, указывающего, когда проявляются побочные эффекты.
Есть ли какая-то часть стандарта, которая затрагивает случаи проявления побочных эффектов, таких как инъекция друга? Или это просто совпадение, что три крупных компилятора делают дружественные инъекции мгновенно видимыми для шаблонов функций auto
, и это может измениться в любой момент?
побочные эффекты появляются в момент создания экземпляра, т. е. при определении dummy1. И Get(FriendIndex<1>)
на самом деле вводится, но это не constexpr
@DavisHerring Не могли бы вы уточнить, что «не все соответствующие случаи являются IFNDR (пока)»? Единственное движение, которое я увидел, - это застойное возражение против инъекции друга 9 лет назад (wg21.cmeerw.net/cwg/issue2118). Есть ли новые разработки?
@Gene Если Get(FriendIndex<1>)
вводится при создании экземпляра, а не constexpr, то почему инъекция Get(FriendIndex<0>)
должна быть constexpr?
@Joshua Maiche - чтобы Get(FriendIndex<1>)
было постоянным выражением, оно должно быть полностью создано и доступно во время компиляции до static_assert. На самом деле я не уверен, что создание экземпляра задерживается до конца.
@Gene Извините, если мой вопрос был неясен, но я хотел задать именно это. Когда инъекции типа Get(FriendIndex<0>)
становятся доступными в шаблонах функций, почему auto
-возвращающие функции изменяются, когда эти инъекции доступны, и есть ли в стандарте что-нибудь, указывающее, почему это происходит?
@Joshua Maiche - у вас есть два места, где имя функции Get объявляется другом. Когда вы упоминаете Get(FriendIndex<1>)
, имя вводится, но определение, необходимое для того, чтобы функция рассматривалась как constexpr, может быть получено только из экземпляра FriendInjector<1, true>
. Этого не произойдет до тех пор, пока DelayedSideEffect
не потребует создания своего экземпляра. Поскольку он имеет известный тип возвращаемого значения, ничто не требует его создания до конца области видимости.
@Joshua Maiche - что мне кажется более интересным, так это то, что clang по-прежнему считает функцию Get не constexpr
после явного создания экземпляра FriendInjector<1, true>
. У других компиляторов с этим все в порядке. godbolt.org/z/Y7K3xd679
@Gene «ничто не требует создания экземпляра до конца области действия» кажется действительно важным. Есть ли в стандарте что-нибудь, указывающее, когда тела экземпляров шаблона будут видны и как auto
может это изменить?
@Gene Что касается твоего примера с богом, я думаю, что это просто махинации ADL. Я подозреваю, что auto dummy2 = Get(FriendIndex<1>{});
каким-то образом добавляет отдельную функцию Get()
в глобальное пространство, а не в пространство FriendIndex
, где FriendInjector
будет определять свою Get()
функцию. Удаление строки dummy2 полностью решает проблему. В этом нет необходимости, поскольку FriendInjector
уже создаст экземпляр FriendIndex
.
Прежде всего, стандарт не различает типы возвращаемых значений int
и auto
таким образом, который соответствует вашему примеру. См. [temp.inst]/3:
Если специализация шаблона функции не была явно создана или явно специализирована, специализация шаблона функции создается неявно, когда на специализацию ссылаются в контексте, который требует существования определения функции. Если вызов не является вызовом явной специализации шаблона функции или функции-члена явно специализированного шаблона класса, аргумент по умолчанию для шаблона функции или функции-члена шаблона класса создается неявно, когда функция вызывается в контексте, который требует значение аргумента по умолчанию.
Когда вы действительно вызываете специализацию шаблона функции, ее определение должно существовать. Это определение не обязательно должно присутствовать в одной и той же единице перевода; он может быть явно создан в другой единице перевода. Но в тех случаях, когда определение присутствует в той же единице перевода, вызов запускает неявное создание экземпляра определения. Итак, в вашем случае при вызове создаются экземпляры как InstantSideEffect
, так и DelayedSideEffect
, а это означает, что вместе с ними создаются экземпляры соответствующих специализаций FriendInjector
.
Однако реализации имеют право отложить создание экземпляров специализаций шаблона функции до более позднего момента в единице перевода в [temp.point]/8:
Специализация для шаблона функции, шаблона функции-члена или функции-члена или члена статических данных шаблона класса может иметь несколько точек создания экземпляров внутри единицы перевода и в дополнение к точкам создания экземпляров, описанных выше, для любого такого специализации, которая имеет точку реализации внутри единицы перевода, конец единицы перевода также считается точкой реализации. Специализация шаблона класса имеет не более одной точки реализации в единице перевода. Специализация для любого шаблона может иметь точки реализации в нескольких единицах перевода. Если две разные точки создания экземпляра придают специализации шаблона разные значения в соответствии с правилом одного определения ([basic.def.odr]), программа является неправильной, и диагностика не требуется.
Последнее предложение по сути означает, что казино всегда выигрывает. Если вы написали код, который зависит от того, находится ли точка создания экземпляра в одном месте, а реализация решает поместить его в другое место, то реализация отклонит ваш код или ваш код будет вести себя не так, как ожидалось, что разрешено, потому что программа имеет неправильную форму, диагностика не требуется. Если реализация решила поместить точку создания экземпляра там, где вы надеялись, то реализация примет ваш код, что ей тоже разрешено делать.
Похоже, что все протестированные вами реализации используют стратегию отсрочки создания экземпляра DelayedSideEffect
до конца единицы перевода, а это означает, что уже слишком поздно, чтобы Get(FriendIndex<1>{})
было постоянным выражением в тот момент, когда оно появляется. Но ваша программа — это IFNDR, потому что ее значение меняется в зависимости от того, откладывается ли создание экземпляра.
С другой стороны, для шаблонных функций, которые имеют выведенный тип возвращаемого значения, не будет отложено создание экземпляра, поскольку компилятору необходимо знать тип функции, прежде чем он сможет продолжить семантический анализ.
Последнее замечание: формулировка в стандарте не совсем верна. Он определяет «точку создания экземпляра», но единственное нормативное значение, придаваемое точкам создания экземпляра, заключается в том, что они управляют поиском имени. Вопрос о том, создан ли экземпляр определения функции или нет, не является проблемой поиска имени. Последнее предложение [temp.point]/8 означает предоставление реализации широкой свободы действий для отсрочки создания экземпляра, не только в отношении поиска имени, поэтому формулировка не совсем отражает намерение.
Правильно ли я понимаю, что «точка создания экземпляра» на самом деле является точкой потенциального создания экземпляра, компилятор не требуется, но ему разрешено создавать экземпляры в этих точках, и программа становится неправильно сформированной только в том случае, если создание экземпляров в разных точках приводит к нарушениям ODR или изменяет смысл программы? Учитывается ли по-прежнему «точка создания экземпляра», даже если создание экземпляра в этой точке приведет к серьезной ошибке?
@Gene Даже теоретически, если два POI могут привести к нарушению ODR, программа будет IFNDR.
@ user12002570 - даже концептуально, чтобы нарушение ODR существовало, вам необходимо иметь два успешных, но разных экземпляра.
«успешные, но разные реализации...» Определите здесь успешные и разные. Означает ли успех отсутствие серьезных ошибок и т. д.? Также разные, например, f<int>()
и f<double>()
или что-то еще.
@Gene Механически, да, вы можете думать о точках создания экземпляров функций как о точках потенциального создания экземпляров. Но согласно стандарту смысл заключается в том, что создание экземпляров происходит во всех точках создания экземпляров, а экземпляры в нескольких точках сравниваются друг с другом, и если они идентичны, программа ведет себя так, как если бы один из них был выбран произвольно.
@Brian Bi - итак, если компилятор действительно выполняет каждое создание экземпляра, это означает, что если какое-либо создание экземпляра завершится неудачно, то компиляция также должна завершиться неудачно. Это верно?
@Gene Да, но компилятор на самом деле не обязан выполнять создание экземпляра на каждом этапе из-за того, что стандарт здесь использует IFNDR.
@Brian Bi - не могли бы вы объяснить 13.9.2/5 (eel.is/c++draft/temp.inst#5), что звучит как указание точного места создания экземпляра, когда говорится, что специализация шаблона функции неявно создается экземпляр, когда на специализацию ссылаются в контексте, который требует существования определения функции, или если существование определения влияет на семантику программы. Это читается так, как будто реализация должна создавать экземпляр, когда упомянутые условия удовлетворены, ни раньше ни после этого.
@Gene IFNDR отменяет все остальные правила стандарта. Если у вас есть программа IFNDR, компилятору всегда разрешено отклонить ее (но это не обязательно), и ему не нужно сообщать вам «истинную» причину, по которой он отклоняет вашу программу. Если программа не является IFNDR, вы не сможете определить, решил ли компилятор отложить создание экземпляра.
@ Брайан Би - извини, у меня еще есть вопросы. IFNDR в цитируемом пункте 14.6.4.1/8 основан на нарушении ODR, отсутствие нарушения ODR = отсутствие IFNDR. В коде OP нет возможности нарушения ODR, независимо от того, задерживает ли компилятор создание экземпляра или нет. Таким образом, следуя этой логике, результат должен зависеть от реализации, а не от IFNDR.
@Gene В нем говорится, что если две разные точки создания экземпляра придают специализации разные значения «в соответствии с правилом одного определения», то программа является IFNDR. Это не просто повторение ODR. Это означает, что вам нужно учитывать результирующую специализацию в каждой точке создания экземпляра и притворяться, что эти многочисленные специализации представляют собой несколько определений одной (встроенной) функции в разных единицах трансляции. И затем, если эта гипотетическая программа нарушает ODR, то исходной программой является IFNDR.
Стандарт очень хочет, чтобы этот вопрос не возникал, хотя не каждый соответствующий случай является IFNDR (пока).