В процессе замены 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
, работающий с неполным типом, определенное поведение? Или нам просто «повезло» с неопределённым поведением?
Во-вторых, если это определено, то почему? Что является неполным в моем понимании шаблонов?
Тип указателя всегда является полным типом. Например, скажем, у нас есть struct Foo;
, тогда Foo*
завершено, а Foo
нет.
Я подозреваю, что этот пример либо демонстрирует неопределенное поведение, либо имеет неправильную форму, и диагностика не требуется (я всегда забываю, что именно). AsBar<Foo*>
имеет две точки создания экземпляра (одна сразу после DoThing
, а другая в конце единицы перевода), которые придают ему разное значение. Ваш компилятор, вероятно, просто выбирает последнюю точку создания экземпляра (если я правильно помню, GCC всегда выбирает ее).
@user12002570 user12002570 Хаха, спасибо за это резюме :)
@IgorTandetnik Точка создания экземпляра также находится в конце единицы перевода. См. Может ли точка создания экземпляра быть отложена до конца единицы перевода?
@PepijnKramer Пожалуйста.
@user12002570 [temp.point]/7 «Если две разные точки создания экземпляра придают специализации шаблона разные значения в соответствии с правилом одного определения (6.3), программа неправильно сформирована, диагностика не требуется». Проблема не в том, что существует две точки реализации, а в том, что смысл функции зависит от того, какая из них выбрана.
@IgorTandetnik Да, и, как объяснено в ссылке, я дал всем трем компиляторам выбрать более позднюю версию, и [temp.point]/7 не нарушается.
@user12002570 user12002570 Тот факт, что три реализации согласны в выборе, не делает программу правильно сформированной в соответствии со стандартом.
@IgorTandetnik Но здесь [temp.point]/7 не нарушается, поскольку нет нарушения одного правила определения.
@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()
в другом.
@IgorTandetnik Я конкретно говорю о данном вопросе. Вы всегда можете создать программу IFNDR, если достаточно постараться и разрешить редактировать вопросы/файлы на C++.
@ user12002570 Я тоже. Я говорю, что не может быть правдой, что std::is_convertible_v<Foo*, Bar*>
имеет одно и то же значение в ODR независимо от того, является ли Foo
полным типом (потому что если бы это было правдой, это привело бы к абсурдным результатам, один из которых я показал; это логический аргумент, доведенный до абсурда). И поэтому [temp.point]/7 действительно применяется, и код в данном вопросе представляет собой неправильно сформированный отчет о недоставке.
@IgorTandetnik Использование inline
еще больше усложнит эту проблему. И поскольку он не используется в данном примере и inline
имеет другое значение (он также используется для предотвращения ошибок множественного определения), и в вашем комментарии было бы неплохо, если бы была рабочая демо-версия, учитывающая намерение этого изменения в стандарте (что сделало точка создания экземпляра в конце TU), я думаю, мы могли бы создать новый отдельный вопрос, в котором будет использоваться ваш пример с минимальным воспроизводимым примером. Чтобы эти два вопроса не путались. Можно конечно дать ссылку на этот.
@IgorTandetnik Кроме того, если вы обнаружите, как именно этот пример нарушает ODR, я бы обновил свой ответ. Но, как я уже сказал, на самом деле это не нарушает его.
@user12002570 user12002570 В разделе ODR никогда не упоминается слово «значение», поэтому не сразу понятно, к чему именно относятся «различные значения в соответствии с правилом одного определения». Однако ODR определяет обстоятельства, при которых два определения классов или два определения встроенных функций не нарушают ODR: такие определения должны состоять из одной и той же последовательности токенов, и существует длинный список дополнительных условий (например, все имена должны разрешаться в одни и те же сущности в обоих местах); другими словами, эти токены должны иметь одно и то же значение. (продолжение)
@ user12002570 Единственный разумный подход, который я вижу, - это сделать вывод, что «две точки создания экземпляров имеют разное значение в соответствии с ODR» следует читать как «если бы эти две точки создания экземпляров были двумя определениями одной и той же функции, они бы нарушили ODR» . Было бы трудно утверждать, что два определения функции, одно из которых фактически содержит static_assert(false)
, а другое static_assert(true)
, не нарушают ODR.
тлдр; Конец единицы перевода считается точкой создания экземпляра, в которой точка Foo
завершена. Кроме того, тип указателя является полным типом.
Во-первых, работает ли шаблонный static_assert для поведения, определенного неполным типом?
Тип указателя является полным типом , потому что размер объекта-указателя не зависит от размера типа, на который указывает
Следующие типы являются неполными типами:
- тип void (возможно, с указанием cv);
- не полностью определенные типы объектов:
- тип класса, который был объявлен (например, путем предварительного объявления), но не определен;
- массив неизвестных границ;
- массив элементов неполного типа;
- тип перечисления с момента объявления до тех пор, пока не будет определен его базовый тип.
Обратите внимание, что ни один из пунктов списка не соответствует/не применим для типа указателя в вашем примере.
Возможно, я что-то упускаю, но почему здесь не работает статическое утверждение? Я согласен, что тип указателя является полным, но его нельзя преобразовать в Bar*
Кроме того, точка создания шаблона функции также находится в конце единицы перевода, где Foo
завершается. Может ли точка создания экземпляра быть отложена до конца единицы перевода?
Вопрос с тегом c Почему разрешены указатели на неполные типы, а не на переменные неполных типов?
Возможно, я что-то упускаю, но почему здесь не работает статическое утверждение? Я согласен, что тип указателя является полным типом, но его нельзя преобразовать в Bar*.
@templatetypedef Потому что точка создания экземпляра шаблона функции также находится в конце единицы перевода, где Foo
становится/завершается. См. Может ли точка создания экземпляра быть отложена до конца единицы перевода?
Даже если типы не являются конвертируемыми, указатели всегда можно преобразовать (даже если при попытке использовать преобразованный указатель результатом будет UB). Возможно, то, что вы собираетесь сделать, больше похоже на проверку: std::is_convbertible_v<T,Bar>
(например, можно ли преобразовать типы, не являющиеся указателями)
@PepijnKramer std::is_convbertible_v<Foo*, Bar*>
выдает ложное для двух совершенно несвязанных типов Foo
и Bar
. Кажется, вы говорите, что он вернет true для любой пары указателей - это не так.
Спасибо! «шаблоны создаются в конце TU» — это деталь, которую мне не хватало, которая объясняет это.
@IgorTandetnik Нет, я не это пытался сказать. Если ОП хочет, чтобы преобразование указателей между несвязанными типами завершилось неудачно, он должен проверять типы, а не типы указателей.
@PepijnKramer Почему «должен»? std::is_convbertible_v<Foo*, Bar*>
тоже не работает для несвязанных типов.
@IgorTandetnik Хорошо, я это пропустил... Я предполагал, что получится. Извините за путаницу
Чтобы внести ясность в реальном коде, у нас нет приведения в стиле C между двумя типами; это просто пример. Умение писать Foo* f = some_bar
— это то, что мы проверяем.
К шаблонам это не имеет никакого отношения. Если вы хотите выполнить приведение к указателю, то 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*>
. Резюме: поскольку у вас есть два указателя, их всегда можно преобразовать.