Определяет ли стандарт, когда видны побочные эффекты создания экземпляра шаблона функции?

Обратите внимание: я не спрашиваю: «Когда создаются экземпляры шаблонов функций?» Скорее: «Когда будут видны побочные эффекты этого создания экземпляра?»

Примером побочного эффекта создания экземпляра может быть внедрение друга. Например:

// 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, и это может измениться в любой момент?

Стандарт очень хочет, чтобы этот вопрос не возникал, хотя не каждый соответствующий случай является IFNDR (пока).

Davis Herring 03.08.2024 01:29

побочные эффекты появляются в момент создания экземпляра, т. е. при определении dummy1. И Get(FriendIndex<1>) на самом деле вводится, но это не constexpr

Gene 03.08.2024 01:43

@DavisHerring Не могли бы вы уточнить, что «не все соответствующие случаи являются IFNDR (пока)»? Единственное движение, которое я увидел, - это застойное возражение против инъекции друга 9 лет назад (wg21.cmeerw.net/cwg/issue2118). Есть ли новые разработки?

Joshua Maiche 03.08.2024 01:48

@Gene Если Get(FriendIndex<1>) вводится при создании экземпляра, а не constexpr, то почему инъекция Get(FriendIndex<0>) должна быть constexpr?

Joshua Maiche 03.08.2024 01:53

@Joshua Maiche - чтобы Get(FriendIndex<1>) было постоянным выражением, оно должно быть полностью создано и доступно во время компиляции до static_assert. На самом деле я не уверен, что создание экземпляра задерживается до конца.

Gene 03.08.2024 01:58

@Gene Извините, если мой вопрос был неясен, но я хотел задать именно это. Когда инъекции типа Get(FriendIndex<0>) становятся доступными в шаблонах функций, почему auto-возвращающие функции изменяются, когда эти инъекции доступны, и есть ли в стандарте что-нибудь, указывающее, почему это происходит?

Joshua Maiche 03.08.2024 02:19

@Joshua Maiche - у вас есть два места, где имя функции Get объявляется другом. Когда вы упоминаете Get(FriendIndex<1>), имя вводится, но определение, необходимое для того, чтобы функция рассматривалась как constexpr, может быть получено только из экземпляра FriendInjector<1, true>. Этого не произойдет до тех пор, пока DelayedSideEffect не потребует создания своего экземпляра. Поскольку он имеет известный тип возвращаемого значения, ничто не требует его создания до конца области видимости.

Gene 03.08.2024 02:24

@Joshua Maiche - что мне кажется более интересным, так это то, что clang по-прежнему считает функцию Get не constexpr после явного создания экземпляра FriendInjector<1, true>. У других компиляторов с этим все в порядке. godbolt.org/z/Y7K3xd679

Gene 03.08.2024 02:33

@Gene «ничто не требует создания экземпляра до конца области действия» кажется действительно важным. Есть ли в стандарте что-нибудь, указывающее, когда тела экземпляров шаблона будут видны и как auto может это изменить?

Joshua Maiche 03.08.2024 03:08

@Gene Что касается твоего примера с богом, я думаю, что это просто махинации ADL. Я подозреваю, что auto dummy2 = Get(FriendIndex<1>{}); каким-то образом добавляет отдельную функцию Get() в глобальное пространство, а не в пространство FriendIndex, где FriendInjector будет определять свою Get() функцию. Удаление строки dummy2 полностью решает проблему. В этом нет необходимости, поскольку FriendInjector уже создаст экземпляр FriendIndex.

Joshua Maiche 03.08.2024 03:08
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
10
108
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Прежде всего, стандарт не различает типы возвращаемых значений int и auto таким образом, который соответствует вашему примеру. См. [temp.inst]/3:

Если специализация шаблона функции не была явно создана или явно специализирована, специализация шаблона функции создается неявно, когда на специализацию ссылаются в контексте, который требует существования определения функции. Если вызов не является вызовом явной специализации шаблона функции или функции-члена явно специализированного шаблона класса, аргумент по умолчанию для шаблона функции или функции-члена шаблона класса создается неявно, когда функция вызывается в контексте, который требует значение аргумента по умолчанию.

Когда вы действительно вызываете специализацию шаблона функции, ее определение должно существовать. Это определение не обязательно должно присутствовать в одной и той же единице перевода; он может быть явно создан в другой единице перевода. Но в тех случаях, когда определение присутствует в той же единице перевода, вызов запускает неявное создание экземпляра определения. Итак, в вашем случае при вызове создаются экземпляры как InstantSideEffect, так и DelayedSideEffect, а это означает, что вместе с ними создаются экземпляры соответствующих специализаций FriendInjector.

Однако реализации имеют право отложить создание экземпляров специализаций шаблона функции до более позднего момента в единице перевода в [temp.point]/8:

Специализация для шаблона функции, шаблона функции-члена или функции-члена или члена статических данных шаблона класса может иметь несколько точек создания экземпляров внутри единицы перевода и в дополнение к точкам создания экземпляров, описанных выше, для любого такого специализации, которая имеет точку реализации внутри единицы перевода, конец единицы перевода также считается точкой реализации. Специализация шаблона класса имеет не более одной точки реализации в единице перевода. Специализация для любого шаблона может иметь точки реализации в нескольких единицах перевода. Если две разные точки создания экземпляра придают специализации шаблона разные значения в соответствии с правилом одного определения ([basic.def.odr]), программа является неправильной, и диагностика не требуется.

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

Похоже, что все протестированные вами реализации используют стратегию отсрочки создания экземпляра DelayedSideEffect до конца единицы перевода, а это означает, что уже слишком поздно, чтобы Get(FriendIndex<1>{}) было постоянным выражением в тот момент, когда оно появляется. Но ваша программа — это IFNDR, потому что ее значение меняется в зависимости от того, откладывается ли создание экземпляра.

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

Последнее замечание: формулировка в стандарте не совсем верна. Он определяет «точку создания экземпляра», но единственное нормативное значение, придаваемое точкам создания экземпляра, заключается в том, что они управляют поиском имени. Вопрос о том, создан ли экземпляр определения функции или нет, не является проблемой поиска имени. Последнее предложение [temp.point]/8 означает предоставление реализации широкой свободы действий для отсрочки создания экземпляра, не только в отношении поиска имени, поэтому формулировка не совсем отражает намерение.

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

Gene 03.08.2024 04:52

@Gene Даже теоретически, если два POI могут привести к нарушению ODR, программа будет IFNDR.

user12002570 03.08.2024 06:49

@ user12002570 - даже концептуально, чтобы нарушение ODR существовало, вам необходимо иметь два успешных, но разных экземпляра.

Gene 03.08.2024 07:22

«успешные, но разные реализации...» Определите здесь успешные и разные. Означает ли успех отсутствие серьезных ошибок и т. д.? Также разные, например, f<int>() и f<double>() или что-то еще.

user12002570 03.08.2024 07:28

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

Brian Bi 03.08.2024 19:29

@Brian Bi - итак, если компилятор действительно выполняет каждое создание экземпляра, это означает, что если какое-либо создание экземпляра завершится неудачно, то компиляция также должна завершиться неудачно. Это верно?

Gene 03.08.2024 19:33

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

Brian Bi 03.08.2024 19:52

@Brian Bi - не могли бы вы объяснить 13.9.2/5 (eel.is/c++draft/temp.inst#5), что звучит как указание точного места создания экземпляра, когда говорится, что специализация шаблона функции неявно создается экземпляр, когда на специализацию ссылаются в контексте, который требует существования определения функции, или если существование определения влияет на семантику программы. Это читается так, как будто реализация должна создавать экземпляр, когда упомянутые условия удовлетворены, ни раньше ни после этого.

Gene 04.08.2024 20:33

@Gene IFNDR отменяет все остальные правила стандарта. Если у вас есть программа IFNDR, компилятору всегда разрешено отклонить ее (но это не обязательно), и ему не нужно сообщать вам «истинную» причину, по которой он отклоняет вашу программу. Если программа не является IFNDR, вы не сможете определить, решил ли компилятор отложить создание экземпляра.

Brian Bi 05.08.2024 02:27

@ Брайан Би - извини, у меня еще есть вопросы. IFNDR в цитируемом пункте 14.6.4.1/8 основан на нарушении ODR, отсутствие нарушения ODR = отсутствие IFNDR. В коде OP нет возможности нарушения ODR, независимо от того, задерживает ли компилятор создание экземпляра или нет. Таким образом, следуя этой логике, результат должен зависеть от реализации, а не от IFNDR.

Gene 06.08.2024 19:58

@Gene В нем говорится, что если две разные точки создания экземпляра придают специализации разные значения «в соответствии с правилом одного определения», то программа является IFNDR. Это не просто повторение ODR. Это означает, что вам нужно учитывать результирующую специализацию в каждой точке создания экземпляра и притворяться, что эти многочисленные специализации представляют собой несколько определений одной (встроенной) функции в разных единицах трансляции. И затем, если эта гипотетическая программа нарушает ODR, то исходной программой является IFNDR.

Brian Bi 06.08.2024 20:50

Другие вопросы по теме