Какое корректное ограничение `enable_if` для установщика совершенной пересылки?
Херб Саттер возвращается к основам! Основы презентации Modern C++ на CppCon обсуждали различные варианты передачи параметров и сравнивали их производительность с простотой написания / обучения. Опция 'advanced' (обеспечивающая наилучшую производительность во всех протестированных случаях, но слишком сложная для большинства разработчиков) была идеальной пересылкой с приведенным примером (PDF, стр. 28):
class employee {
std::string name_;
public:
template <class String,
class = std::enable_if_t<!std::is_same<std::decay_t<String>,
std::string>::value>>
void set_name(String &&name) noexcept(
std::is_nothrow_assignable<std::string &, String>::value) {
name_ = std::forward<String>(name);
}
};
В примере используется функция шаблона со ссылкой для пересылки с параметром шаблона String
ограничено использованием enable_if
, Однако ограничение представляется неправильным: кажется, говорят, что этот метод может быть использован только если String
тип не std::string
, что не имеет смысла. Это будет означать, что это std::string
член может быть установлен с помощью чего угодно, кроме std::string
значение.
using namespace std::string_literals;
employee e;
e.set_name("Bob"s); // error
Одним из объяснений, которое я рассмотрел, было то, что есть простая опечатка, и ограничение должно было быть std::is_same<std::decay_t<String>, std::string>::value
вместо !std::is_same<std::decay_t<String>, std::string>::value
, Однако это будет означать, что сеттер не работает, например, с const char *
и очевидно, что он предназначен для работы с этим типом, учитывая, что это один из случаев, протестированных в презентации.
Мне кажется, что правильное ограничение больше похоже на:
template <class String,
class = std::enable_if_t<std::is_assignable<decltype((name_)),
String>::value>>
void set_name(String &&name) noexcept(
std::is_nothrow_assignable<decltype((name_)), String>::value) {
name_ = std::forward<String>(name);
}
разрешить использовать все, что может быть назначено члену, с установщиком.
У меня есть правильное ограничение? Есть ли другие улучшения, которые можно сделать? Есть ли какое-то объяснение первоначальному ограничению, возможно, оно было вырвано из контекста?
Также мне интересно, действительно ли сложные, "необучаемые" части этой декларации действительно так полезны. Поскольку мы не используем перегрузку, мы можем просто положиться на обычную реализацию шаблона:
template <class String>
void set_name(String &&name) noexcept(
std::is_nothrow_assignable<decltype((name_)), String>::value) {
name_ = std::forward<String>(name);
}
И, конечно, есть некоторые споры о том, noexcept
действительно имеет значение, некоторые говорят, что не стоит беспокоиться об этом, за исключением примитивов перемещения / обмена:
template <class String>
void set_name(String &&name) {
name_ = std::forward<String>(name);
}
Возможно, с концепциями было бы неоправданно сложно ограничить шаблон, просто для улучшенных сообщений об ошибках.
template <class String>
requires std::is_assignable<decltype((name_)), String>::value
void set_name(String &&name) {
name_ = std::forward<String>(name);
}
Это по-прежнему имеет недостатки, заключающиеся в том, что он не может быть виртуальным и что он должен быть в заголовке (хотя, надеюсь, модули в конечном итоге будут отображать этот спор), но это кажется вполне обучаемым.
3 ответа
Я думаю, что вы, вероятно, правы, но в интересах не писать "ответ", который просто "Я согласен", я вместо этого предложу это, чтобы проверить назначение на основе правильных типов - будь то lval, rval, const, что угодно:
template <class String>
auto set_name(String&& name)
-> decltype(name_ = std::forward<String>(name), void()) {
name_ = std::forward<String>(name);
}
Одним из объяснений, которое я рассмотрел, было то, что есть простая опечатка, и ограничение должно было быть
std::is_same<std::decay_t<String>, std::string>::value
вместо!std::is_same<std::decay_t<String>, std::string>::value
,
Да, правильное ограничение, которое было представлено на экране, было is_same
не !is_same
, Похоже, на вашем слайде есть опечатка.
Однако это будет означать, что сеттер не работает, например, с
const char *
Да, и я считаю, что это было сделано специально. Когда строковый литерал, как "foo"
передается в функцию, принимающую универсальную ссылку, тогда выводимый тип не является указателем (так как массивы распадаются на указатели только при попадании в параметр шаблона по значению), скорее это const char(&)[N]
, Тем не менее, каждый звонок set_name
со строковым литералом различной длины создаст экземпляр нового set_name
специализация, как:
void set_name(const char (&name)[4]); // set_name("foo");
void set_name(const char (&name)[5]); // set_name("foof");
void set_name(const char (&name)[7]); // set_name("foofoo");
Предполагается, что ограничение определяет универсальную ссылку так, чтобы она принимала и выводила только std::string
типы для аргументов rvalue или cv- std::string&
для аргументов lvalue (вот почему std::decay
перед сравнением с std::string
в этом std::is_same
состояние).
очевидно, что он предназначен для работы с этим типом, учитывая, что это один из случаев, протестированных в презентации.
Я думаю, что протестированная версия (4) не была ограничена (обратите внимание, что она была названа String&&
+ идеальная пересылка), так что это может быть так просто, как:
template <typename String>
void set_name(String&& name)
{
_name = std::forward<String>(name);
}
так что когда передается строковый литерал, он не создает std::string
экземпляр перед вызовом функции, как это делают версии без шаблонов (излишнее выделение памяти в коде вызываемого просто для создания std::string
временный, который в конечном итоге будет перемещен в возможно заранее выделенный пункт назначения, как _name
):
void set_name(const std::string& name);
void set_name(std::string&& name);
Мне кажется, что правильное ограничение больше похоже на:
std::enable_if_t<std::is_assignable<decltype((name_)), String>::value>>
Нет, как я уже писал, я не думаю, что целью было ограничить set_name
принимать типы, присваиваемые std::string
, Еще раз - это оригинал std::enable_if
есть ли иметь одну реализацию set_name
функция, принимающая только универсальную ссылку std::string
rvalues и lvalues (и ничего более, хотя это шаблон). В вашей версии std::enable_if
передача чего-либо не назначаемого std::string
будет выдавать ошибку, независимо от того, является ли это константой или нет при попытке сделать это. Обратите внимание, что в конечном итоге мы могли бы просто двигаться name
аргумент _name
если это неконстантная ссылка на значение, то проверка назначаемости бессмысленна, за исключением ситуаций, когда мы не используем SFINAE для исключения этой функции из разрешения перегрузки в пользу другой перегрузки.
Что правильно
enable_if
ограничение на совершенную переадресацию?
template <class String,
class = std::enable_if_t<std::is_same<std::decay_t<String>,
std::string>::value>>
или вообще никаких ограничений, если это не приведет к снижению производительности (как передача строковых литералов).
Я пытался уместить эти мысли в комментарии, но они не подходят. Я полагаю, что напишу это, как я упоминал как в комментариях выше, так и в великолепном выступлении Херба.
Извините, что опоздал на вечеринку. Я просмотрел свои заметки и действительно виновен в том, что рекомендовал Хербу первоначальное ограничение для варианта 4 минус ошибочный !
, Никто не идеален, как наверняка подтвердит моя жена, меньше всего меня.:-)
Напоминание: точка зрения Херба (с чем я согласен) - начать с простого совета C++98/03
set_name(const string& name);
и двигаться оттуда только по мере необходимости. Вариант № 4 довольно немного движения. Если мы рассмотрим вариант № 4, мы приступим к подсчету нагрузок, хранилищ и распределений в критической части приложения. Нам нужно назначить name
в name_
как можно быстрее. Если мы здесь, читаемость кода гораздо менее важна, чем производительность, хотя правильность все еще важна.
Не предоставляя никаких ограничений (для варианта № 4), я считаю, что это немного неправильно. Если какой-то другой код попытается ограничить себя тем, может ли он вызывать employee::set_name
, он может получить неправильный ответ, если set_name
совсем не ограничен:
template <class String>
auto foo(employee& e, String&& name)
-> decltype(e.set_name(std::forward<String>(name)), void()) {
e.set_name(std::forward<String>(name));
// ...
Если set_name
без ограничений и String
выводит к некоторому совершенно не связанному типу X
вышеуказанное ограничение на foo
неправильно включает в себя этот экземпляр foo
в комплекте перегрузки. И правильность все еще король...
Что делать, если мы хотим назначить один символ name_
? Сказать A
, Это должно быть разрешено? Должен ли он быть злым быстро?
e.set_name('A');
А почему бы не?! std::string
имеет только такой оператор присваивания:
basic_string& operator=(value_type c);
Но обратите внимание, что нет соответствующего конструктора:
basic_string(value_type c); // This does not exist
Следовательно is_convertible<char, string>{}
является false
, но is_assignable<string, char>{}
является true
,
Это не логическая ошибка, чтобы попытаться установить имя string
с char
(если вы не хотите добавлять документацию в employee
это говорит так). Таким образом, даже при том, что оригинальная реализация C++98/03 не допускала синтаксис:
e.set_name('A');
Это разрешило ту же логическую операцию менее эффективным способом:
e.set_name(std::string(1, 'A'));
И мы имеем дело с вариантом № 4, потому что мы отчаянно пытаемся оптимизировать эту вещь в максимально возможной степени.
По этим причинам я думаю is_assignable
это лучшая черта, чтобы ограничить эту функцию. И стилистически я нахожу технику Барри для написания этого ограничения вполне приемлемой. Поэтому здесь идет мой голос.
Также обратите внимание, что employee
а также std::string
Вот только примеры в разговоре Херба. Они являются заменой для типов, с которыми вы работаете в своем коде. Этот совет предназначен для обобщения кода, с которым вам приходится иметь дело.