Подтвердить что ты не робот

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

С++ 11

14.8.2 - Вывод из аргумента шаблона - [temp.deduct]

7 Подстановка происходит во всех типах и выражениях, которые используются в типе функции и в объявлениях параметров шаблона. Выражения включают в себя не только постоянные выражения, такие как те, которые появляются в границах массива или как аргументы шаблона nontype, но также общие выражения (т.е. Непостоянные выражения) внутри sizeof, decltype и другие контексты, которые допускают непостоянные выражения.


С++ 14

14.8.2 - Вывод из аргумента шаблона - [temp.deduct]

7 Подстановка происходит во всех типах и выражениях, которые используются в типе функции и в объявлениях параметров шаблона. Выражения включают в себя не только постоянные выражения, такие как те, которые появляются в границах массива или как аргументы шаблона nontype, но также общие выражения (т.е. Непостоянные выражения) внутри sizeof, decltype и другие контексты, которые допускают непостоянные выражения, Подстановка продолжается в лексическом порядке и останавливается, когда встречается условие, которое вызывает отказ вывода.



В добавленном предложении явно указывается порядок подстановки при работе с параметрами шаблона в С++ 14.

Порядок замещения - это то, что чаще всего не уделяется большого внимания. Мне еще предстоит найти один документ о том, почему это имеет значение. Возможно, это потому, что С++ 1y еще не полностью стандартизирован, но я предполагаю, что такое изменение должно было быть введено по какой-то причине.

Вопрос:

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

Ответ 1

Как указано, С++ 14 явно говорит о том, что порядок замены шаблона аргументам четко определен; более конкретно, будет гарантировано действовать в лексическом порядке и останавливаться всякий раз, когда замещение приводит к сбою вывода.

По сравнению с С++ 11 будет намного проще писать SFINAE-код, который состоит из одного правила в зависимости от другого в С++ 14, мы также удалимся от случаев, когда undefined упорядочение замены шаблона может сделать все наше приложение страдает от undefined -behaviour.

Примечание. Важно отметить, что поведение, описанное в С++ 14, всегда было предполагаемым поведением, даже в С++ 11, просто так, что оно не было написано таким явным образом.



В чем причина такого изменения?

Исходную причину этого изменения можно найти в отчете о дефекте, первоначально представленном Даниэлем Крюглером:


ДАЛЬНЕЙШЕЕ ПОЯСНЕНИЕ

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

Ошибка замены не является ошибкой, но просто... "aw, это не сработало.. пожалуйста, перейдите".

Проблема заключается в том, что потенциальные недопустимые типы и выражения рассматриваются только в непосредственном контексте подстановки.

14.8.2 - Вывод из аргумента шаблона - [temp.deduct]

8 Если подстановка приводит к недопустимому типу или выражению, вывод типа не выполняется. Недопустимый тип или выражение - это тот, который был бы плохо сформирован, если он написан с использованием замещенных аргументов.

     

[ Примечание: Проверка доступа выполняется как часть процесса замещения. - примечание конца]

     

Только недопустимые типы и выражения в непосредственном контексте типа функции и ее типов параметров шаблона могут привести к ошибке дедукции.

     

[ Примечание.. Оценка замещенных типов и выражений может приводить к побочным эффектам, таким как создание специализированных специализированных шаблонов классов и/или функций, генерация неявно определенных функции и т.д. Такие побочные эффекты не находятся в "непосредственном контексте" и могут привести к плохой форме программы. - примечание конца]

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

В частности, может быть разница между наличием шаблона, который применим в SFINAE, и шаблон, который не.


Пример SILLY

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.



Какие последствия будут иметь для разработки кода в С++ 14?

Это изменение значительно облегчит жизнь юристов-языковедов, пытающихся реализовать что-то, что гарантированно будет оцениваться определенным образом (и порядком), независимо от того, какой соответствующий компилятор они используют.

Он также сделает замену шаблонных аргументов более естественным способом для неязыковых юристов; наличие замещения происходит от слева направо, гораздо более интуитивно понятное, чем erhm-like-any-way-the-compiler-wanna-do-it-like-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); }

Это облегчит нашу боль и, похоже, сделает именно то, что мы хотим; мы переходим в счетчик и возвращаем базовое значение.

Мы говорим себе, что эта реализация является удивительной и попросить нашего коллегу (Дон Кихота) сесть и проанализировать нашу реализацию, прежде чем выпустить ее в производство.


ОБЗОР КОДА

Дон Кихот - опытный разработчик на С++, который имеет чашку кофе в одной руке и стандарт С++ в другом. Это тайна, как ему удается написать одну строку кода, обе руки заняты, но это другая история.

Он просматривает наш код и приходит к выводу, что реализация небезопасна, нам нужно защитить std::underlying_type от undefined -behaviour, так как мы можем передать в T, который не относится к типу перечисления.

20.10.7.6 - Другие преобразования - [meta.trans.other]

template<class T> struct underlying_type;
     

Условие: T должно быть перечисляющим типом (7.2)
   Комментарии: Элемент typedef type должен указывать базовый тип T.

Примечание. Стандарт определяет условие для underlying_type, но не идет дальше, чтобы указать, что произойдет, если оно создается экземпляром с не-перечислением. Поскольку мы не знаем, что произойдет в таком случае, использование будет под undefined -behavior; это может быть чистый UB, сделать приложение плохо сформированным или заказать съедобное нижнее белье онлайн.


НОЧЬ В БОРЬБЕ С БОЛЕЕ

Дон что-то кричит о том, как мы всегда должны соблюдать стандарт С++, и что мы должны чувствовать огромный позор за то, что мы сделали.. это неприемлемо.

После того, как он успокоился и получил еще несколько глотков кофе, он предлагает нам изменить реализацию, чтобы добавить защиту от создания экземпляра 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); }

WINDMILL

Мы благодарим Дона за его открытия и теперь удовлетворены нашей реализацией, но только до тех пор, пока мы не поймем, что порядок замены шаблонных аргументов не определен в С++ 11 (и не указано, когда замещение остановится).

Скомпилированный как С++ 11, наша реализация может по-прежнему создавать экземпляр std::underlying_type с T, который не относится к типу перечисления из-за двух причин:

  • Компилятор может свободно оценивать (D) до (C), так как порядок подстановки не определен корректно и

  • даже если компилятор оценивает (C) до (D), он не гарантирует, что он не будет оценивать (D), С++ 11 не имеет предложения, явно говорящего, когда цепочка замещения должна останавливаться.


Реализация Don будет свободна от undefined -behavior в С++ 14, но только потому, что С++ 14 явно заявляет, что подстановка будет действовать в лексическом порядке и что она будет останавливаться всякий раз, когда подстановка вызывает вывод сбой.

Дон не мог сражаться с ветряными мельницами на этом, но он наверняка пропустил очень важный дракон в стандарте С++ 11.

Допустимая реализация в С++ 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 был использован, потому что это простой способ использовать что-то в стандарте против того, что находится в стандарте; важный бит заключается в том, что создание экземпляра с не-перечислением - это поведение undefined.

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