Какое правильное ограничение `enable_if` на идеальном устройстве для пересылки?
Трава Саттер Назад к основам! В Essentials Modern С++ презентации на 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
, некоторые говорят, что не слишком беспокоятся об этом, кроме примитивов move/swap:
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);
}
Это все равно будет иметь недостатки, что он не может быть виртуальным и что он должен быть в заголовке (хотя, надеюсь, модули в конечном итоге выведут этот спор), но это кажется довольно обучаемым.
Ответы
Ответ 1
Я думаю, что вы, вероятно, правы, но в интересах не писать "ответ", который просто "я согласен", я предлагаю это вместо этого, чтобы проверить назначение на основе правильных типов - будь то lval, rval, const, что угодно:
template <class String>
auto set_name(String&& name)
-> decltype(name_ = std::forward<String>(name), void()) {
name_ = std::forward<String>(name);
}
Ответ 2
Одно из объяснений, которое я рассматривал, было то, что там была простая опечатка, а ограничение было 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
ed перед сравнением с std::string
в этом условии std::is_same
).
он, очевидно, должен был работать с этим типом, учитывая, что один из случаев, проверенных в презентации.
Я думаю, что проверенная версия (4) не была ограничена (обратите внимание, что она была названа String&&
+ совершенная переадресация), поэтому она может быть такой же простой, как:
template <typename String>
void set_name(String&& name)
{
_name = std::forward<String>(name);
}
так что, когда передается строковый литерал, он не создает экземпляр std::string
до вызова функции, как это делали бы не templated версии (излишне выделяя память в код вызываемого кода только для того, чтобы построить временный 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
, если он ссылается не на константу rvalue, поэтому проверка назначаемости бессмысленна, за исключением ситуаций, когда мы не используем SFINAE, чтобы исключить эту функцию из разрешения перегрузки в пользу другая перегрузка.
Какое правильное ограничение enable_if
на идеальном устройстве переадресации?
template <class String,
class = std::enable_if_t<std::is_same<std::decay_t<String>,
std::string>::value>>
или вообще не иметь ограничений, если это не вызовет какого-либо повышения производительности (подобно передаче строковых литералов).
Ответ 3
Я попытался вставить эти мысли в комментарий, но они не подходят. Я предполагаю написать это, поскольку я упоминал как в комментариях выше, так и в Великом разговоре Херба.
Жаль, что опаздывала на вечеринку. Я просмотрел свои заметки, и действительно, я виноват в том, что рекомендую Herb исходное ограничение для варианта 4 за вычетом !
. Никто не идеален, поскольку моя жена, несомненно, подтвердит, и меньше всего меня.: -)
Напоминание: точка травления (с которой я согласен) начинается с простого совета С++ 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
, которая говорит так). Поэтому, хотя исходная реализация С++ 98/03 не позволила синтаксису:
e.set_name('A');
Это позволило сделать такую же логическую операцию менее эффективным:
e.set_name(std::string(1, 'A'));
И мы имеем дело с вариантом № 4, потому что мы отчаянно пытаемся оптимизировать эту вещь в максимально возможной степени.
По этим причинам я считаю, что is_assignable
- лучшая черта для ограничения этой функции. И стилистически я нахожу метод Барри для написания этого ограничения вполне приемлемым. Поэтому в этом мой голос идет.
Также обратите внимание, что employee
и std::string
здесь просто примеры в разговорах Herb. Они представляют собой stand-ins для типов в вашем коде. Этот совет предназначен для обобщения кода, с которым вам приходится иметь дело.