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

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

Вот простая версия, казалось бы, работающего static_assert:

#include <type_traits>

struct Bar {};

template<typename T>
inline Bar* AsBar(T* ptr)
{
    static_assert(std::is_convertible_v<T*, Bar*>);
    return (Bar*)ptr;
}

struct Foo;

void DoThing(Foo* f)
{
    AsBar(f);
}

struct Foo : public Bar {};

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

Итак, у меня есть несколько вопросов:

Во-первых, имеет ли шаблон static_assert, работающий с неполным типом, определенное поведение? Или нам просто «повезло» с неопределённым поведением?

Во-вторых, если это определено, то почему? Что является неполным в моем понимании шаблонов?

К шаблонам это не имеет никакого отношения. Если вы хотите выполнить приведение к указателю, то C++ с радостью позволяет вам это сделать, полный тип не требуется (размер указателя известен во время компиляции, и этого достаточно). Примечание: НЕ используйте приведения в стиле «C» в современном коде C++, например. (Bar*) должно быть reinterpret_cast<Bar*>. Также не используйте static_assert таким образом, используйте SFINAE или концепции для удаления шаблонов функций в качестве кандидатов. auto* AsBar(T* prt) -> std::enable_if_t<std::is_convertible_v<T*,Bar*>,Bar*> . Резюме: поскольку у вас есть два указателя, их всегда можно преобразовать.

Pepijn Kramer 24.06.2024 05:27

Тип указателя всегда является полным типом. Например, скажем, у нас есть struct Foo;, тогда Foo* завершено, а Foo нет.

user12002570 24.06.2024 05:34

Я подозреваю, что этот пример либо демонстрирует неопределенное поведение, либо имеет неправильную форму, и диагностика не требуется (я всегда забываю, что именно). AsBar<Foo*> имеет две точки создания экземпляра (одна сразу после DoThing, а другая в конце единицы перевода), которые придают ему разное значение. Ваш компилятор, вероятно, просто выбирает последнюю точку создания экземпляра (если я правильно помню, GCC всегда выбирает ее).

Igor Tandetnik 24.06.2024 05:53

@user12002570 user12002570 Хаха, спасибо за это резюме :)

Pepijn Kramer 24.06.2024 05:54

@IgorTandetnik Точка создания экземпляра также находится в конце единицы перевода. См. Может ли точка создания экземпляра быть отложена до конца единицы перевода?

user12002570 24.06.2024 05:56

@PepijnKramer Пожалуйста.

user12002570 24.06.2024 05:57

@user12002570 [temp.point]/7 «Если две разные точки создания экземпляра придают специализации шаблона разные значения в соответствии с правилом одного определения (6.3), программа неправильно сформирована, диагностика не требуется». Проблема не в том, что существует две точки реализации, а в том, что смысл функции зависит от того, какая из них выбрана.

Igor Tandetnik 24.06.2024 05:59

@IgorTandetnik Да, и, как объяснено в ссылке, я дал всем трем компиляторам выбрать более позднюю версию, и [temp.point]/7 не нарушается.

user12002570 24.06.2024 06:00

@user12002570 user12002570 Тот факт, что три реализации согласны в выборе, не делает программу правильно сформированной в соответствии со стандартом.

Igor Tandetnik 24.06.2024 06:01

@IgorTandetnik Но здесь [temp.point]/7 не нарушается, поскольку нет нарушения одного правила определения.

user12002570 24.06.2024 06:03

@user12002570 user12002570 Сейчас я не могу найти главу и стих, но это не может быть правдой. Конечно, если я определяю что-то вроде inline std::integral_constant<bool, std::is_convertible_v<Foo*,Bar*>> f() в двух единицах перевода, где в одной Foo и Bar неполные, а в другой они полные и Foo происходит от Bar, это должно быть нарушением ODR - по сути, я объявляю std::true_type f() в одном месте и std::false_type f() в другом.

Igor Tandetnik 24.06.2024 06:17

@IgorTandetnik Я конкретно говорю о данном вопросе. Вы всегда можете создать программу IFNDR, если достаточно постараться и разрешить редактировать вопросы/файлы на C++.

user12002570 24.06.2024 06:20

@ user12002570 Я тоже. Я говорю, что не может быть правдой, что std::is_convertible_v<Foo*, Bar*> имеет одно и то же значение в ODR независимо от того, является ли Foo полным типом (потому что если бы это было правдой, это привело бы к абсурдным результатам, один из которых я показал; это логический аргумент, доведенный до абсурда). И поэтому [temp.point]/7 действительно применяется, и код в данном вопросе представляет собой неправильно сформированный отчет о недоставке.

Igor Tandetnik 24.06.2024 06:26

@IgorTandetnik Использование inline еще больше усложнит эту проблему. И поскольку он не используется в данном примере и inline имеет другое значение (он также используется для предотвращения ошибок множественного определения), и в вашем комментарии было бы неплохо, если бы была рабочая демо-версия, учитывающая намерение этого изменения в стандарте (что сделало точка создания экземпляра в конце TU), я думаю, мы могли бы создать новый отдельный вопрос, в котором будет использоваться ваш пример с минимальным воспроизводимым примером. Чтобы эти два вопроса не путались. Можно конечно дать ссылку на этот.

user12002570 24.06.2024 06:33

@IgorTandetnik Кроме того, если вы обнаружите, как именно этот пример нарушает ODR, я бы обновил свой ответ. Но, как я уже сказал, на самом деле это не нарушает его.

user12002570 24.06.2024 06:34

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

Igor Tandetnik 24.06.2024 06:54

@ user12002570 Единственный разумный подход, который я вижу, - это сделать вывод, что «две точки создания экземпляров имеют разное значение в соответствии с ODR» следует читать как «если бы эти две точки создания экземпляров были двумя определениями одной и той же функции, они бы нарушили ODR» . Было бы трудно утверждать, что два определения функции, одно из которых фактически содержит static_assert(false), а другое static_assert(true), не нарушают ODR.

Igor Tandetnik 24.06.2024 06:57
Стоит ли изучать 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
17
125
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

тлдр; Конец единицы перевода считается точкой создания экземпляра, в которой точка Foo завершена. Кроме того, тип указателя является полным типом.


Во-первых, работает ли шаблонный static_assert для поведения, определенного неполным типом?

Тип указателя является полным типом , потому что размер объекта-указателя не зависит от размера типа, на который указывает

Следующие типы являются неполными типами:

  • тип void (возможно, с указанием cv);
  • не полностью определенные типы объектов:
    • тип класса, который был объявлен (например, путем предварительного объявления), но не определен;
    • массив неизвестных границ;
    • массив элементов неполного типа;
    • тип перечисления с момента объявления до тех пор, пока не будет определен его базовый тип.

Обратите внимание, что ни один из пунктов списка не соответствует/не применим для типа указателя в вашем примере.


Возможно, я что-то упускаю, но почему здесь не работает статическое утверждение? Я согласен, что тип указателя является полным, но его нельзя преобразовать в Bar*

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


Вопрос с тегом c Почему разрешены указатели на неполные типы, а не на переменные неполных типов?

Возможно, я что-то упускаю, но почему здесь не работает статическое утверждение? Я согласен, что тип указателя является полным типом, но его нельзя преобразовать в Bar*.

templatetypedef 24.06.2024 05:48

@templatetypedef Потому что точка создания экземпляра шаблона функции также находится в конце единицы перевода, где Foo становится/завершается. См. Может ли точка создания экземпляра быть отложена до конца единицы перевода?

user12002570 24.06.2024 05:52

Даже если типы не являются конвертируемыми, указатели всегда можно преобразовать (даже если при попытке использовать преобразованный указатель результатом будет UB). Возможно, то, что вы собираетесь сделать, больше похоже на проверку: std::is_convbertible_v<T,Bar> (например, можно ли преобразовать типы, не являющиеся указателями)

Pepijn Kramer 24.06.2024 05:56

@PepijnKramer std::is_convbertible_v<Foo*, Bar*>выдает ложное для двух совершенно несвязанных типов Foo и Bar. Кажется, вы говорите, что он вернет true для любой пары указателей - это не так.

Igor Tandetnik 24.06.2024 06:05

Спасибо! «шаблоны создаются в конце TU» — это деталь, которую мне не хватало, которая объясняет это.

James Picone 24.06.2024 06:06

@IgorTandetnik Нет, я не это пытался сказать. Если ОП хочет, чтобы преобразование указателей между несвязанными типами завершилось неудачно, он должен проверять типы, а не типы указателей.

Pepijn Kramer 24.06.2024 06:11

@PepijnKramer Почему «должен»? std::is_convbertible_v<Foo*, Bar*> тоже не работает для несвязанных типов.

Igor Tandetnik 24.06.2024 06:13

@IgorTandetnik Хорошо, я это пропустил... Я предполагал, что получится. Извините за путаницу

Pepijn Kramer 24.06.2024 06:15

Чтобы внести ясность в реальном коде, у нас нет приведения в стиле C между двумя типами; это просто пример. Умение писать Foo* f = some_bar — это то, что мы проверяем.

James Picone 24.06.2024 06:17

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