Почему порядок подстановки аргументов шаблона имеет значение?
C++11
14.8.2 -Вывод Аргумента Шаблона -
[temp.deduct]
7 подстановка происходит во всех типах и выражениях, которые используются в типе функции и в объявлениях параметров шаблона. Выражения включают в себя не только постоянные выражения, такие как те, которые появляются в границах массива или в качестве аргументов шаблона nontype, но и общие выражения (т. е. непостоянный выражения) внутри
sizeof
,decltype
, и другие контексты, которые допускают непостоянные выражения.
C++14
14.8.2 -Вывод Аргумента Шаблона -
[temp.deduct]
7 подстановка происходит во всех типах и выражениях, которые используются в типе функции и в объявлениях параметров шаблона. Выражения включают в себя не только постоянные такие выражения, как те, которые появляются в границы массива или в качестве шаблона нетиповые аргументы, а также общего выражения (т. е. непостоянные выражения) внутри
sizeof
,decltype
, и другие контексты, которые допускают непостоянные выражения. подстановка происходит в лексическом порядке и останавливается, когда встречается условие, которое вызывает сбой дедукции.
добавленное предложение явно указывает порядок замены при работе с параметрами шаблона в C++14.
порядок замены-это то, что чаще всего не уделяется много внимания. Я еще не нашел ни одной статьи о том, почему это имеет значение. Возможно, это связано с тем, что C++1y еще не был полностью стандартизирован, но я предполагаю, что такое изменение должно было быть введено по какой-то причине.
вопрос:
- почему и когда порядок подстановки аргументов шаблона имеет значение?
1 ответ:
как указано C++14 явно говорит, что порядок подстановки аргументов шаблона четко определен; более конкретно, он будет гарантированно продолжаться в "лексико ордер и останавливаться всякий раз, когда замена вызывает сбой вычета.
по сравнению с C++11 писать будет намного проще SFINAE-код, который состоит из одного правила в зависимости от другого в C++14, мы также отойдем от случаев, когда неопределенный порядок шаблона замена может сделать все наше приложение страдает от неопределенного поведения.
Примечание: важно отметить, что поведение, описанное в C++14, всегда было предполагаемым поведением, даже в C++11, просто оно не было сформулировано таким явным образом.
каково обоснование такого изменения?
первоначальная причина этого изменения может быть найдена в отчет о неисправности первоначально представлено Даниил Krügler:
ДАЛЬНЕЙШЕЕ ОБЪЯСНЕНИЕ
при написании SFINAE мы, как разработчики, зависит от компилятор, чтобы найти любую замену, которая приведет к недопустимому тип или выражение в нашем шаблоне при использовании. Если такая недопустимая сущность найдена, мы хотели бы игнорировать все, что объявляет шаблон, и перейти к надежде найти подходящее совпадение.
Ошибка Подстановки Не Является Ошибкой, но простой.. "Ой, это не сработало.. пожалуйста, двигайтесь дальше".
проблема в том, что потенциальные недопустимые типы и выражения ищутся только в непосредственный контекст о замене.
14.8.2 -Вычет Аргумента Шаблона -
[temp.deduct]
8 если подстановка приводит к недопустимому типу или выражению, вывод типа завершается ошибкой. Недопустимый тип или выражение-это тот, который был бы неверно сформирован, если бы был написан с использованием подставленных аргументов.
[ Примечание: проверка доступа выполняется как часть процесса подстановки. -- end note]
только недопустимые типы и выражения в непосредственном контексте типа функции и его типов параметров шаблона могут привести к ошибке вычитания.
[ Примечание: оценка замещенных типов и выражений может привести к побочным эффектам, таким как создание экземпляра класса специализации шаблона и/или шаблон функции специализации, создание неявно определенных функций и т. д. Такие побочные эффекты не находятся в" непосредственном контексте " и могут привести к неправильному формированию программы. -- end note]
другими словами подстановка, которая происходит в не-непосредственный контекст по-прежнему будет делать программу плохо сформированной, поэтому порядок подстановок шаблонов важен; это может изменить весь смысл определенного шаблона.
более конкретно это может быть разница между наличием шаблон, который и можно использовать в SFINAE, и шаблон, который не.
ГЛУПЫЙ ПРИМЕР
template<typename SomeType> struct inner_type { typedef typename SomeType::type type; };
template< class T, class = typename T::type, // (E) class U = typename inner_type<T>::type // (F) > void foo (int); // preferred
template<class> void foo (...); // fallback
struct A { }; struct B { using type = A; }; int main () { foo<A> (0); // (G), should call "fallback " foo<B> (0); // (H), should call "preferred" }
на линии отмечено
(G)
мы нужно, чтобы компилятор сначала проверил(E)
и если это удастся оценить(F)
, но до стандартного изменения, обсуждаемого в этом посте, такой гарантии не было.
непосредственный контекст замен в
foo(int)
входит:
(E)
убедившись, что прошло вT
и::type
(F)
убедившись, чтоinner_type<T>
есть::type
если
(F)
оценивается, хотя(E)
приводит к недопустимой замене, или если(F)
оценивается до(E)
наш короткий (глупый) пример не будет использовать SFINAE, и мы получим диагностическое сообщение о том, что наше приложение плохо сформировано.. несмотря на то, что мы намеревалисьfoo(...)
использовать в таком случае.
Примечание: обратите внимание, что
SomeType::type
не в непосредственный контекст шаблона; сбой в typedef внутриinner_type
сделает приложение плохо сформированным и не позволит шаблону использовать SFINAE.
какие последствия это будет иметь для разработки кода в C++14?
изменение резко облегчит жизнь язык-адвокатов попытка реализовать то, что гарантированно будет оценивается определенным образом (и в определенном порядке), независимо от того, какой соответствующий компилятор они используют.
это также заставит замену аргументов шаблона вести себя более естественным образом неязыковых-адвокатов; если замена происходит из слева направо гораздо более интуитивно, чем erhm-как-ни-как-с-компилятора-хочу-сделать-это-как-erhm-....
нет ли каких-либо негативных подтекст?
единственное, что я могу придумать, это то, что поскольку порядок замены будет происходить от слева направо компилятор не может обрабатывать несколько подстановок одновременно, используя асинхронную реализацию.
мне еще предстоит наткнуться на такую реализацию, и я сомневаюсь, что это приведет к какому-либо значительному увеличению производительности, но, по крайней мере, мысль (в теории) подходит к "негативной" стороне вещи.
в качестве примера: компилятор не сможет использовать два потока, которые одновременно делают замены при мгновенном создании определенного шаблона без какого-либо механизма, чтобы действовать как замены, которые произошли после определенного момента никогда не происходило, если это требуется.
история
Примечание: пример, который можно было бы взять из реальной жизни, будет представлен в этом разделе, чтобы описать, когда и почему порядок подстановки аргументов шаблона имеет значение. Пожалуйста, дайте мне знать (через разделе комментариев) если что-то не достаточно ясно, или, может быть, даже неправильно.
представьте, что мы работаем с счетчики и что мы хотели бы легко получить базовый стоимостью указанного перечисление.
в основном мы больны и устали от того, чтобы всегда писать
(A)
, когда мы в идеале хотели бы что-то ближе к(B)
.auto value = static_cast<std::underlying_type<EnumType>::type> (SOME_ENUM_VALUE); // (A)
auto value = underlying_value (SOME_ENUM_VALUE); // (B)
ОРИГИНАЛЬНАЯ РЕАЛИЗАЦИЯ
сказано и сделано, мы решили написать реализацию
underlying_value
смотреть ниже.template<class T, class U = typename std::underlying_type<T>::type> U underlying_value (T enum_value) { return static_cast<U> (enum_value); }
это облегчит нашу боль и, похоже, сделает именно то, что мы хотим; мы передаем перечислитель и возвращаем базовое значение.
мы говорим себе что эта реализация является удивительным и спросить коллегу нашего (Дон Кихот), чтобы сесть и рассмотреть нашу реализацию, прежде чем толкать его в производство.
ОБЗОР КОДА
Дон Кихот является опытным разработчиком C++, который имеет чашку кофе в одной руке, а стандарт C++ в другой. Это тайна, как ему удается написать одну строку кода с обеими руками заняты, но это другая история.
он просматривает наш код и приходит к выводу, что реализация небезопасна, нам нужно охранять
std::underlying_type
от неопределенного поведения, так как мы можем пройти вT
не тип перечисления.20.10.7.6 -Другие Преобразования -
[meta.trans.other]
template<class T> struct underlying_type;
состояние:
T
должен быть тип перечисления (7.2)
комментарии: член typedeftype
назовем базовый типT
.Примечание: стандарт устанавливает условие на
underlying_type
, но он не идет дальше, чтобы уточнить, что произойдет, если он будет создан с помощью без перечисления. Поскольку мы не знаем, что произойдет в таком случае использование подпадает под неопределенное поведение; это может быть чисто UB, Сделайте приложение плохо сформированным или закажите съедобное нижнее белье онлайн.
РЫЦАРЬ В СИЯЮЩИХ ДОСПЕХАХ
Дон кричит что-то о том, как мы всегда должны соблюдать стандарт C++, и что мы должны чувствовать огромный стыд за то, что мы сделали.. это неприемлемо.
после того, как он успокоился и сделал еще несколько глотков кофе, он предлагает нам изменить реализацию, чтобы добавить защиту против создания экземпляра
std::underlying_type
С чем-то, что не допускается.template< typename T, typename = typename std::enable_if<std::is_enum<T>::value>::type, // (C) typename U = typename std::underlying_type<T>::type // (D) > U underlying_value (T value) { return static_cast<U> (value); }
ВЕТРЯНАЯ МЕЛЬНИЦА
мы благодарим Дона за его открытия и теперь удовлетворены нашей реализацией, но только до тех пор, пока мы не поймем, что порядок замены аргументов шаблона не четко определен в C++11 (и не указано, когда замена остановится).
скомпилированный как C++11 наша реализация все еще может вызвать создание экземпляра
std::underlying_type
с помощьюT
не перечисление тип из-за двух причин:
компилятор может свободно оценивать
(D)
до(C)
поскольку порядок подстановки не является четко определенным, и;даже если компилятор вычисляет
(C)
до(D)
, это не гарантирует, что он не будет оценивать(D)
в C++11 нет оговорки, прямо говоря, если замена цепи остановка.
реализация Доном будет свободна от неопределенное поведение в C++14, но только потому, что c++14 прямо говорится, что замена перейти в лексическом порядке, и что он будет остановка всякий раз, когда замена вызывает вычет не удается.
Дон, возможно, не сражается с ветряными мельницами на этом, но он наверняка пропустил очень важного дракона в C++11 норматив.
допустимая реализация в C++11 должна была бы убедиться, что независимо от порядка, в котором происходит замена параметров шаблона, установка
std::underlying_type
не будет с недопустимым типом.#include <type_traits> namespace impl { template<bool B, typename T> struct underlying_type { }; template<typename T> struct underlying_type<true, T> : std::underlying_type<T> { }; } template<typename T> struct underlying_type_if_enum : impl::underlying_type<std::is_enum<T>::value, T> { }; template<typename T, typename U = typename underlying_type_if_enum<T>::type> U get_underlying_value (T value) { return static_cast<U> (value); }
Примечание:
underlying_type
был использован, потому что это простой способ использовать что-то в стандарте против того, что находится в стандарте; важным битом является то, что его экземпляр с без перечисления is неопределенное поведение.The дефект-отчет ранее в этом посте использует гораздо более сложный пример, который предполагает обширные знания о материи. Я надеюсь, что эта история является более подходящим объяснением для тех, кто не очень хорошо читал на эту тему.