Каково правильное ограничение "enable if" на perfect forwarding setter?

Херба Саттера вернемся к основам! Основы современного C++ презентация на CppCon обсудила различные варианты прохождения параметров и сравнила их производительность с простотой написания/обучения. "Продвинутый" вариант (обеспечивающий лучшую производительность во всех проверенных случаях, но слишком сложный для большинства разработчиков) был идеальной переадресацией, с приведенным примером (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 член может быть установлен с помощью ничего, кроме an 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::decayed, прежде чем сравнивать с std::string в этой std::is_same состояние).


это, очевидно, предназначался для работы с этим типом учитывая, что это один из случаев, проверенных в демонстрация.

я думаю, что тестированная версия (4) не была ограничена (обратите внимание, что она была названа String&&+perfect forwarding), так что это может быть так же просто, как:

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 принять видах назначается to std::string. Еще раз-что оригинально std::enable_if есть ли одна реализация взяв универсальный справочник прием только std::string'ы rvalues и lvalues (и ничего больше, хотя это шаблон). В вашей версии std::enable_if передача чего-либо не присваиваемого std::string приведет к ошибке независимо от того, является ли константа или нет при попытке сделать это. Обратите внимание, что в конечном итоге мы можем просто переместить это до _name если это non-const rvalue reference, поэтому проверка назначаемости бессмысленна, за исключением ситуаций, когда мы не используем SFINAE для исключения этой функции из перегрузки разрешение в пользу другой перегрузки.


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

template <class String,
          class = std::enable_if_t<std::is_same<std::decay_t<String>,
                                                std::string>::value>>

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


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

Извините, что опоздал на вечеринку. Я просмотрел свои заметки, и действительно, я виновен в том, что рекомендую Herb исходное ограничение для варианта 4 минус заблуждающийся !. Никто не совершенен, что, несомненно, подтвердит моя жена, и меньше всего я. :-)

напоминание: точка Херба (с которой я согласен С) начать с простого совета C++98/03

set_name(const string& name);

и двигаться оттуда только по мере необходимости. Вариант № 4-это совсем немного движения. Если мы рассматриваем вариант №4, мы до подсчета нагрузок, магазинов и распределений в критической части приложения. Нам нужно назначить name to 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>{} is true.

это не логическая ошибка при попытке установить имя string С char (если вы не хотите добавить документацию в employee это так говорит). Поэтому, хотя исходная реализация C++98/03 не допускала синтаксиса:

e.set_name('A');

это позволило той же логической операции менее эффективным образом:

e.set_name(std::string(1, 'A'));

и мы имеем дело с вариантом №4, потому что мы отчаянно пытаемся оптимизировать эту вещь в максимально возможной степени.

по этим причинам я думаю is_assignable самая лучшая черта для ограничить эту функцию. И стилистически я нахожу техника Барри для написания этого ограничения вполне приемлема. Поэтому я голосую за это.

Также обратите внимание, что employee и std::string вот только примеры с Херба. Они замещают виды вы разобраться с in код код. Этот совет предназначен для обобщения кода, с которым вы должны иметь дело.