Почему make_optional разлагает свой тип аргумента?

Объект (возможно, не С++ 14, возможно, библиотека TS) make_optional определен (в n3672) как:

template <class T>
  constexpr optional<typename decay<T>::type> make_optional(T&& v) {
    return optional<typename decay<T>::type>(std::forward<T>(v));
  }

Почему необходимо преобразовать тип T (т.е. не просто возвращать optional<T>), и существует ли философское (а также практическое) обоснование использования decay в частности как преобразование?

Ответы

Ответ 1

Общая цель decay - взять тип и изменить его, чтобы он был подходящим для хранения.

Взгляните на эти примеры, что decay выполняет работу, а remove_reference не будет:

auto foo( std::string const& s ) {
  if (global_condition)
    return make_optional( s );
  else
    return {};
}

или

void function() { std::cout << "hello world!\n"; }
auto bar() { return std::make_optional( function ); }

или

int buff[15];
auto baz() { return std::make_optional( buff ); }

An optional<int[15]> был бы очень странным зверем - массивы стиля C плохо себя чувствуют, когда обрабатываются как литералы, что optional делает с его параметром T.

Если вы делаете копию данных, природа источника const или volatile не имеет значения. И вы можете сделать только простую копию массивов и функций, разложив их на указатели (не отступая от std::array или подобных). (теоретически можно было бы работать, чтобы сделать работу optional<int[15]>, но было бы много дополнительных осложнений)

Итак, std::decay решает все эти проблемы и на самом деле не вызывает проблем, если вы позволяете make_optional выводить свой тип аргумента, а не передавать T буквально.

Если вы хотите передать T буквально, нет никаких оснований использовать make_optional в конце концов.

Ответ 2

Это не новое поведение с make_optional; функции утилиты make_pair и make_tuple ведут себя одинаково. Для этого я вижу как минимум две причины:

  • Может быть нежелательно или невозможно создать экземпляр шаблона с нераскрытыми типами.

    • Если T - это тип функции, ну, вы просто не можете хранить функцию внутри класса, периода; но вы можете сохранить указатель на функцию (разложившийся тип).

    • Если T - тип массива: результирующий класс будет "дефектным", потому что он не может быть скопирован из-за того, что массивы не могут быть назначены для копирования. Для optional функция члена value_or не может быть скомпилирована вообще, потому что она возвращает T. Следовательно, вы не можете иметь тип массива в optional вообще.

  • Неразлагающиеся типы могут привести к неожиданному поведению.

    • Если аргумент является строковым литералом, я лично ожидал, что тип будет const char*, а не const char [n]. Этот распад происходит в большинстве мест, так почему бы и нет здесь?

    • Если v является lvalue, тогда T будет выводиться как ссылочный тип lvalue. Действительно ли я хочу пару, содержащую ссылку, например, только потому, что один из аргументов был lvalue? Наверняка нет.

    • Тип внутри пары или кортежа или необязательный или что-то другое не должно приобретать cv-квалификацию v. То есть, скажем, x объявлен как const int. Должен ли make_optional(x) создать optional<const int>? Нет, не должно; он должен создать optional<int>.

Ответ 3

Просто посмотрите, что делает decay:

  • Удаляет контрольные квалификаторы - T&T; нет дополнительных ссылок, иначе вы не смогли бы переустановить optional, который должен быть назначен.

  • Для типов массивов удаляет экстент - T[42]T*; необязательные массивы с фиксированной степенью сложности, которые полезны, потому что каждый размер массива имеет другой тип, и вы не можете напрямую передавать типы массивов по значению, необходимые для работы функций-членов value и value_or.

  • Для типов функций добавляет указатель- T(U)T(*)(U); без дополнительных ссылок на функции по тем же причинам.

  • В противном случае удаляются cv-qualifiers- const intint; нет дополнительных const значений, опять же, иначе вы не смогли бы переустановить optional.