Какое правильное ограничение `enable_if` для установщика совершенной пересылки?

Херб Саттерс Вернуться к истокам! Основы современного 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);
}

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

15

Решение

Я думаю, что вы, вероятно, правы, но в интересах не писать «ответ», который просто «Я согласен», я вместо этого предложу это, чтобы проверить назначение на основе правильных типов — будь то lval, rval, const, что угодно:

template <class String>
auto set_name(String&& name)
-> decltype(name_ = std::forward<String>(name), void()) {
name_ = std::forward<String>(name);
}
3

Другие решения

Одним из объяснений, которое я рассмотрел, было то, что есть простая опечатка, и ограничение должно было быть 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 или резюме-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::stringrvalues ​​и 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

Я пытался уместить эти мысли в комментарии, но они не подходят. Я предполагаю написать это, как я упоминал как в комментариях выше, так и в великолепном выступлении Херба.

Извините, что опоздал на вечеринку. Я просмотрел свои записи и действительно рекомендую Хербу исходное ограничение для варианта 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 Вот только примеры в разговоре Херба. Они являются заменой для типов вы иметь дело с в ваш код. Этот совет предназначен для обобщения кода, с которым вам приходится иметь дело.

1
По вопросам рекламы [email protected]