Как стать автором
Обновить

Ликбез по передаче параметров по значению в конструкторы и сеттеры (современный C++, примеры)

Время на прочтение4 мин
Количество просмотров20K
Судя по комментам habr.com/ru/post/460831/#comment_20416435 в соседнем посте и развернувшейся там дискуссии, на Хабре не помешает статья, как правильно передавать аргументы в конструктор или сеттер. На StackOverflow подобного материала полно, но тут что-то я не припомню.

Потому что пример в той статье полностью корректен, и автор статьи абсолютно прав. Вот этот пример:

// Хорошо.
struct person {
  person(std::string first_name, std::string last_name)
    : first_name{std::move(first_name)} // верно
    , last_name{std::move(last_name)} // std::move здесь СУЩЕСТВЕНЕН!
  {}
private:
  std::string first_name;
  std::string last_name;
};

Такой код позволяет покрыть все (ну ладно, почти все) возможные варианты использования класса:

std::string first{"abc"}, last{"def"};
person p1{first, last};  // (1) копирование обеих строк
person p2{std::move(first), last}; // !!! копирование только второй
person p2{std::move(first), std::move(last)}; // (3) нет копирования
person p3{"x", "y"}; // нет копирования

Сравните со старым методом, когда передавали по const&: он однозначно хуже, потому что исключает вариант (3):

// Плоховато.
struct person {
  person(const std::string& first_name, const std::string& last_name)
    : first_name{first_name}
    , last_name{last_name}
  {}
private:
  std::string first_name;
  std::string last_name;
};

std::string first{"abc"}, last{"def"};
person p1{first, last}; // будет копирование, как и хотели

// Но что если мы точно знаем, что first и last нам больше не
// понадобятся? мы не можем их в таком случае переместить
// и добиться 0 копирований! Поэтому const& хуже.

Альтернативный вариант с && также хуже, но в обратную сторону:

// Странновато.
struct person {
  person(std::string&& first_name, std::string&& last_name)
    : first_name{std::move(first_name)}
    , last_name{std::move(last_name)}
  {}
private:
  std::string first_name;
  std::string last_name;
};

std::string first{"abc"}, last{"def"};
person p1{std::move(first), std::move(last)}; // норм
// но вот если мы НЕ хотим перемещения, то в случае && придется хитрить:
person p2{std::string{first}, std::string{last}}; // FOOOO

Если не боитесь комбинаторного взрыва, то можете дать шанс && (но зачем? реального выигрыша в скорости не будет никакого, оптимизатор не дремлет):

// Пожалейте свою клавиатуру.
struct person {
  person(std::string&& first_name, std::string&& last_name)
    : first_name{std::move(first_name)}
    , last_name{std::move(last_name)} {}
  person(const std::string& first_name, std::string&& last_name)
    : first_name{first_name}
    , last_name{std::move(last_name)} {}
  person(std::string&& first_name, const std::string& last_name)
    : first_name{std::move(first_name)}
    , last_name{last_name} {}
  person(const std::string& first_name, const std::string& last_name)
    : first_name{first_name}
    , last_name{last_name} {}
private:
  std::string first_name;
  std::string last_name;
};

Или вот то же самое, только с шаблонами (но опять же, зачем?):

// Заумно в данном случае (из пушки по воробьям), хотя в других может быть норм.
struct person {
  template <typename T1, typename T2>
  person(T1&& first_name, T2&& last_name)
    : first_name{std::forward<T1>(first_name)}
    , last_name{std::forward<T2>(last_name)} {}
private:
  std::string first_name;
  std::string last_name;
};

Даже если у вас не std::string, а какой-то объект собственноручно написанного большущего класса, и вы хотите людей заставить перемещать его (а не копировать), то в таком случае лучше запретить конструктор копирование у этого большущего класса, нежели везде передавать его по &&. Так надежнее, да и код короче.

Напоследок пара вариантов, как делать НЕ СТОИТ:

// Кошмарно.
struct person {
  person(const std::string& first_name, const std::string& last_name)
    : first_name{first_name}
    , last_name{last_name}
  {}
private:
  // НЕТ и НЕТ: это мегаопасно, никогда не сохраняйте константные
  // ссылки в свойствах объекта
  const std::string& first_name;
  const std::string& last_name;
};

person p{"x", "y"}; // ха-ха-ха, приехали

И так тоже не надо:

// Плоховато.
struct person {
  person(std::string& first_name, std::string& last_name)
    : first_name{first_name}
    , last_name{last_name}
  {}
private:
  // так можно иногда, но лучше все же воспользоваться shared_ptr:
  // будет медленнее, но безопасно
  std::string& first_name;
  std::string& last_name;
};

Почему так происходит, каков фундаментальный принцип? Он прост: объект, как правило, должен ВЛАДЕТЬ своими свойствами.

Если объект не хочет чем-то владеть, то он может владеть shared_ptr'ом на это «что-то». Кстати, shared_ptr'ы в таком случае тоже надо передавать по значению, а не по константной ссылке — тут разницы с самым первым примером в начале статьи никакой:

// Лучше (если нет выхода).
struct person {
  person(std::shared_ptr<portfolio> pf) 
    : pf{std::move(pf)} // std::move здесь важен для производительности
  {}
private:
  std::shared_ptr<portfolio> pf;
};

auto psh = std::make_shared<portfolio>("xxx", "yyy", "zzz");
...
person p1{psh};
person p2{std::move(psh)}; // (X) так эффективнее, если psh больше не нужен

Обратите внимание: std::move для shared_ptr совершенно легален, он исключает накладные расходы на блокировку счетчика ссылок shared_ptr в памяти (сотни циклов CPU) и на его инкремент. Он никак не влияет на время жизни объекта и остальные ссылки на него. Но (X) можно делать, конечно, только в случае, если ссылка psh в коде ниже нам больше не нужна.

Мораль: не используйте const& повально. Смотрите по обстоятельствам.

P.S.
Используйте {} вместо () при передаче параметров конструктора. Модно, современно, молодежно.

P.P.S.
В заключение еще одна вещь: std::move() на самом деле ничего не перемещает и сам по себе транслируется в ноль ассемблерных инструкций. Все, что делает std::move(), — это как бы навешивает специальный «липкий ярлык» на ссылку, превращая ее в && — rvalue reference. И дальше можно с этим ярлыком отдельно «мэтчить» тип параметра функции (например, иметь отдельную перегрузку функции для &&-параметра и отдельную для &-параметра). Смысл &&-ярлыка — дать возможность вызывающему коду сказать вызываемому: «если хочешь, ты можешь съесть значение по этой ссылке, оно мне больше не нужно; но только если съешь, косточки-то оставь, мне еще деструктор потом вызывать для оставшегося скелета». С тем же успехом можно было бы передавать и обычные &-ссылки (по ним же тоже можно объект «съесть»), но с && получается лучше семантика, т.к. не перепутаешь: где можно съесть, а где можно только понюхать.

В этой связи название std::move() следует признать крайне неудачным. Правильно было бы назвать его std::eat_me_if_you_want() или std::bon_appetit(). Но std::move() короче.
Теги:
Хабы:
Всего голосов 46: ↑45 и ↓1+44
Комментарии76

Публикации