Почему значение, принимающее функции члена setter, не рекомендуется в разговоре Herb Sutter CppCon 2014 (Назад к основам: современный стиль С++)?

В Herb Sutter CppCon 2014 поговорим Назад к основам: Современный стиль С++ он ссылается на слайд 28 (веб-копию слайдов здесь) к этому шаблону:

class employee {
  std::string name_;
public:
  void set_name(std::string name) noexcept { name_ = std::move(name); }
};

Он говорит, что это проблематично, потому что при вызове set_name() с временной, noexcept-ness не сильно (он использует фразу "noexcept-ish" ).

Теперь я использовал приведенный выше шаблон довольно сильно в своем собственном недавнем коде на С++, главным образом потому, что он сохраняет меня, набирая две копии set_name() каждый раз - да, я знаю, что это может быть немного неэффективно, заставляя построить копию каждый раз, но эй, я ленивый типер. Однако фраза Herb "Это noexcept проблематична" меня беспокоит, так как проблема здесь не решена: std::string оператор переадресации переносится noexcept, как и его деструктор, поэтому set_name() выше кажется мне гарантированным noexcept. Я вижу потенциальный выброс исключения компилятором перед set_name() по мере того, как он подготавливает параметр, но я изо всех сил стараюсь увидеть это как проблематичное.

Позже на слайде 32 Herb ясно заявляет, что это анти-шаблон. Может кто-нибудь объяснить мне, почему я не писал плохой код, будучи ленивым?

Ответ 1

Другие рассмотрели приведенные выше аргументы noexcept.

Трава потратила гораздо больше времени на разговоры об аспектах эффективности. Проблема заключается не в распределении, а в отсутствии ненужных освобождений. Когда вы копируете один std::string в другой, процедура копирования повторно использует выделенное хранилище целевой строки, если есть достаточно места для хранения копируемых данных. При выполнении назначения перемещения необходимо удалить выделенную строку существующей памяти, поскольку она берет память из исходной строки. Идиома "копировать и перемещать" заставляет освобождение всегда происходить, даже если вы не проходите мимо временного. Это источник ужасной производительности, которая демонстрируется позже в разговоре. Его совет заключался в том, чтобы вместо этого использовать const ref, и если вы определите, что вам нужно, у него есть перегрузка для ссылок на r-значение. Это даст вам лучшее из обоих миров: скопируйте в существующее хранилище для не-временных, избегая освобождения и перейдите на временные рамки, где вы собираетесь платить за освобождение так или иначе (либо назначение освобождается до перехода, либо источник освобождается после копирования).

Вышеприведенное не относится к конструкторам, поскольку в переменной-члене нет места для освобождения. Это хорошо, поскольку конструкторы часто принимают более одного аргумента, и если вам нужно выполнить пересылку ref ref/r-value ref для каждого аргумента, вы получите комбинаторный взрыв перегрузок конструктора.

Теперь возникает вопрос: сколько классов существует для повторного использования хранилища, например std::string при копировании? Я предполагаю, что std::vector делает, но за пределами этого я не уверен. Я знаю, что я никогда не писал класс, который повторно использует хранилище, но я написал много классов, содержащих строки и векторы. Следуя рекомендациям Herb, вы не повредите вам классы, которые не будут повторно использовать память, вы будете сначала копировать с копирующей версией функции приемника, и если вы определите, что копирование слишком сильно ударит по производительности, сделайте опорную перегрузку r-значения, чтобы избежать копирования (как и для std::string). С другой стороны, использование "copy-and-move" имеет продемонстрированное значение производительности для std::string и других типов, которые повторно используют хранилище, и эти типы, вероятно, видят много использования кода большинства людей. Я сейчас следую совету Херба, но мне нужно немного подумать об этом немного, прежде чем я полностью рассмотрю вопрос о том, что проблема полностью решена (возможно, сообщение в блоге, которое у меня нет времени писать, скрывается во всем этом).

Ответ 2

Были рассмотрены две причины: почему передача по значению может быть лучше, чем передача по ссылке const.

  • более эффективный
  • noexcept

В случае сеттеров для членов типа std::string он опроверг утверждение о том, что переход по значению был более эффективным, показывая, что передача по ссылке const обычно приводит к меньшему количеству распределений (по крайней мере, для std::string).

Он также опроверг утверждение о том, что он позволяет setter быть noexcept, показывая, что объявление noexcept вводит в заблуждение, поскольку в процессе копирования параметра все еще может возникать исключение.

Таким образом, он пришел к выводу, что переход по ссылке const должен был быть предпочтительнее передачи по значению, по крайней мере в этом случае. Однако он упомянул, что переход по значению был потенциально хорошим подходом для конструкторов.

Я думаю, что одного примера для std::string недостаточно для обобщения ко всем типам, но он ставит под сомнение практику передачи дорогостоящих копий, но дешевых для перемещения параметров по стоимости, по крайней мере по причинам эффективности и исключения.

Ответ 3

У Herb есть точка в том, что принятие значения, когда у вас уже есть выделенное хранилище, может быть неэффективным и вызвать ненужное распределение. Но принятие const& почти так же плохо, как если бы вы взяли необработанную строку C и передали ее функции, возникает ненужное распределение.

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

Теперь вы можете сделать это как template:

class employee {
  std::string name_;
public:
  template<class T>
  void set_name(T&& name) noexcept { name_ = std::forward<T>(name); }
};

который является достаточно эффективным. Затем добавьте SFINAE, возможно:

class employee {
  std::string name_;
public:
  template<class T>
  std::enable_if_t<std::is_convertible<T,std::string>::value>
  set_name(T&& name) noexcept { name_ = std::forward<T>(name); }
};

поэтому мы получаем ошибки в интерфейсе, а не в реализации.

Это не всегда практично, так как оно требует публичной публикации.

Здесь может быть класс типа string_view:

template<class C>
struct string_view {
  // could be private:
  C const* b=nullptr;
  C const* e=nullptr;

  // key component:
  C const* begin() const { return b; }
  C const* end() const { return e; }

  // extra bonus utility:
  C const& front() const { return *b; }
  C const& back() const { return *std::prev(e); }

  std::size_t size() const { return e-b; }
  bool empty() const { return b==e; }

  C const& operator[](std::size_t i){return b[i];}

  // these just work:
  string_view() = default;
  string_view(string_view const&)=default;
  string_view&operator=(string_view const&)=default;

  // myriad of constructors:
  string_view(C const* s, C const* f):b(s),e(f) {}

  // known continuous memory containers:
  template<std::size_t N>
  string_view(const C(&arr)[N]):string_view(arr, arr+N){}
  template<std::size_t N>
  string_view(std::array<C, N> const& arr):string_view(arr.data(), arr.data()+N){}
  template<std::size_t N>
  string_view(std::array<C const, N> const& arr):string_view(arr.data(), arr.data()+N){}
  template<class... Ts>
  string_view(std::basic_string<C, Ts...> const& str):string_view(str.data(), str.data()+str.size()){}
  template<class... Ts>
  string_view(std::vector<C, Ts...> const& vec):string_view(vec.data(), vec.data()+vec.size()){}
  string_view(C const* str):string_view(str, str+len(str)) {}
private:
  // helper method:
  static std::size_t len(C const* str) {
    std::size_t r = 0;
    if (!str) return r;
    while (*str++) {
      ++r;
    }
    return r;
  }
};

такой объект может быть построен непосредственно из std::string или "raw C string" и почти без сбоев хранить то, что вам нужно знать, чтобы создать из него новый std::string.

class employee {
  std::string name_;
public:
  void set_name(string_view<char> name) noexcept { name_.assign(name.begin(),name.end()); }
};

и поскольку теперь наш set_name имеет фиксированный интерфейс (не идеальный форвард), он может не реализовать свою реализацию.

Единственная неэффективность заключается в том, что если вы передаете указатель строки в стиле C, вы, разумеется, немного обойдете свой размер дважды (сначала найдите '\0', второй раз скопируйте их). С другой стороны, это дает вашу целевую информацию о том, насколько она велика, поэтому она может заранее распределить, а не перераспределять.

Ответ 4

У вас есть два способа вызова этих методов.

  • С параметром rvalue, если move constructor типа параметра не существует, нет проблем (в случае std::string, скорее всего, это не исключение), в любом случае лучше использовать условное noexcept ( чтобы убедиться, что параметры не являются исключением)
  • С параметром lvalue в этом случае будет вызываться copy constructor типа параметра и почти уверен, что ему потребуется некоторое выделение (которое может быть выбрано).

В таких случаях, когда использование может быть пропущено, лучше избегать. Клиент class предполагает, что исключение не выбрано как указано, но в допустимом, компилируемом, а не подозрительном C++11 может генерировать.