Почему порядок подстановки аргументов шаблона имеет значение?


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 56

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)
комментарии: член typedef type назовем базовый тип 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 не перечисление тип из-за двух причин:

  1. компилятор может свободно оценивать (D) до (C) поскольку порядок подстановки не является четко определенным, и;

  2. даже если компилятор вычисляет (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 дефект-отчет ранее в этом посте использует гораздо более сложный пример, который предполагает обширные знания о материи. Я надеюсь, что эта история является более подходящим объяснением для тех, кто не очень хорошо читал на эту тему.