Подтвердить что ты не робот

Передача по значениям vs const & и && перегрузки

Итак, после поиска семантики перемещения я вижу, что общий консенсус должен пройти по стоимости, когда вы намерены передать право собственности. Но в Скотт Мейер говорит об универсальных ссылках Я заметил, что std::vector::push_back имеет 2 перегрузки:

void push_back( const T& value );
void push_back( T&& value );

Итак, я подумал про себя: не хватит ли void push_back( T value );? Я попросил несколько человек, которые в конечном итоге приводят к следующему тестовому примеру:

#include <memory>
#include <iostream>
#include <type_traits>

struct A
{
    A() { std::cout << "A Default constructor\n"; }
    A(const A &) { std::cout << "A Copy\n"; }
    A(A &&) { std::cout << "A Move\n"; }
};

std::aligned_storage<sizeof(A)> contents;
A& alias = *reinterpret_cast<A*>(&contents);

void ByVal(A a)
{
    new (&contents) A(std::move(a));
    alias.~A();
}

void ByLCRef(A const& a)
{
    new (&contents) A(a);
    alias.~A();
}

void ByRRef(A&& a)
{
    new (&contents) A(std::move(a));
    alias.~A();
}

int main()
{
    A a;
    std::cout << "\n";
    std::cout << "ByVal(a);\n";
    ByVal(a);
    std::cout << "ByVal(std::move(a));\n";
    ByVal(std::move(a));
    std::cout << "ByVal(A());\n";
    ByVal(A());
    std::cout << "ByLCRef(a);\n";
    ByLCRef(a);
    std::cout << "ByRRef(std::move(a));\n";
    ByRRef(std::move(a));
    std::cout << "ByRRef(A());\n";
    ByRRef(A());
}

Что дает следующее:

A Default constructor

ByVal(a);
A Copy
A Move
ByVal(std::move(a));
A Move
A Move
ByVal(A());
A Default constructor
A Move
ByLCRef(a);
A Copy
ByRRef(std::move(a));
A Move
ByRRef(A());
A Default constructor
A Move

Как вы можете видеть, ByVal производит 1 дополнительный ход по сравнению с парой эталонных перегрузок. Поэтому вопрос: стоит ли это? Когда вы создадите две перегрузки вместо одного простого прохода по функции значений?

4b9b3361

Ответ 1

Как вы можете видеть, ByVal производит 1 дополнительный ход по сравнению с парой опорные перегрузки. Поэтому вопрос: стоит ли это? Когда вы создаете две перегрузки вместо одного простого прохода по функции значений?

+1 Большинство людей, которые задают этот вопрос, не утруждают себя анализом. Таким образом, вы получаете мой взнос за выполнение домашних заданий.: -)

Стоит ли это или нет, будет зависеть от стоимости конструктора перемещения и от количества аргументов, которые выполняет функция. С одной стороны, если конструктор перемещения не так быстр, вам может быть очень важно избавиться от них (в пользу решения const && & overload). С другой стороны, если ваша функция имеет 4 параметра, каждая из которых нуждается в обработке lvalue/rvalue, возможно, вы не захотите написать 16 перегрузок для покрытия всех случаев. Это много кода для поддержки, а встроенная сложность кода - это приглашение для ошибок. Таким образом, подход, основанный на стоимости, выглядит более привлекательным (что не требует перегрузок).

Итак, imho, нет общего ответа на вопрос "стоит ли это". Лучшим ответом является оснащение себя знаниями о стоимости каждого решения, как вы уже сделали, и выработать инженерное решение в каждом конкретном случае.

Обновление

В случае vector<T>::push_back imho const && & решение перегрузки стоит того. Существует только один параметр, и мы не знаем, насколько дорог конструктор перемещения. В самом деле, мы даже не знаем , если есть конструктор перемещения. Измените свой эксперимент, чтобы проверить этот последний случай (удаление конструктора перемещения):

ByVal(a);
A Copy
A Copy

ByLCRef(a);
A Copy

Вы хотите заплатить одну копию или две, чтобы скопировать A в vector?

т.е. чем меньше вы знаете о своих параметрах, тем больше вы должны ориентироваться на сторону производительности, особенно если вы пишете что-то, что сильно используется как std::vector.

Ответ 2

Важным моментом является то, что клиентский код не нужно изменять при переключении между передачей по значению и перегрузкой. Таким образом, это действительно сводится к производительности - vs-maintenance. И поскольку техническое обслуживание обычно предпочтительнее, я придумал следующее эмпирическое правило:

Переход по значению, если:
1. Перемещение конструктора или назначение перемещения не является тривиальным.
2. Объект является копируемым, но не движимым.
3. Вы пишете библиотеку шаблонов и не знаете тип объекта.
4. Несмотря на то, что объект имеет тривиальный конструктор перемещения и назначение, ваш профилировщик все еще показывает вам, что программа проводит много времени внутри ходов.

Ответ 3

Хранение подвижного и копируемого класса

Представьте, что у вас есть этот класс:

class Data {
 public:
  Data() { }
  Data(const Data& data)            { std::cout << "  copy constructor\n";} 
  Data(Data&& data)                 { std::cout << "  move constructor\n";}
  Data& operator=(const Data& data) { std::cout << "  copy assignment\n"; return *this;}
  Data& operator=(Data&& data)      { std::cout << "  move assignment\n"; return *this;}  
};

Обратите внимание, хороший компилятор С++ 11 должен определять все эти функции для вас (некоторые старые версии Visual Studio этого не делают), но я определяю их здесь для вывода отладки.

Теперь, если вы хотите написать класс для хранения одного из этих классов, я мог бы использовать передачу по значению, как вы предлагаете:

class DataStore {
  Data data_;
 public: 
  void setData(Data data) { data_ = std::move(data); }
};

Я использую семантику перемещения С++ 11 для перемещения значения в нужное место. Затем я могу использовать этот DataStore следующим образом:

  Data d;   
  DataStore ds;

  std::cout << "DataStore test:\n";
  ds.setData(d);

  std::cout << "DataStore test with rvalue:\n";
  ds.setData(Data{});

  Data d2;
  std::cout << "DataStore test with move:\n";
  ds.setData(std::move(d2));

Который имеет следующий вывод:

DataStore test:
  copy constructor
  move assignment
DataStore test with rvalue:
  move assignment
DataStore test with move:
  move constructor
  move assignment

Что хорошо. У меня есть два хода в последнем тесте, которые могут быть неоптимальными, но ходы, как правило, дешевые, поэтому я могу с этим смириться. Чтобы сделать его более оптимальным, нам нужно было бы перегрузить функцию setData что мы сделаем позже, но это, вероятно, преждевременная оптимизация на данном этапе.

Хранение неподвижного класса

Но теперь представьте, что у нас есть копируемый, но неподвижный класс:

class UnmovableData {
 public:
  UnmovableData() { }
  UnmovableData(const UnmovableData& data) { std::cout << "  copy constructor\n";}
  UnmovableData& operator=(const UnmovableData& data) { std::cout << "  copy assignment\n"; return *this;}  
};

До С++ 11 все классы были неподвижны, так что ожидайте найти их в дикой природе сегодня. Если бы мне нужно было написать класс для хранения этого, я бы не смог воспользоваться семантикой перемещения, поэтому я бы написал что-то вроде этого:

class UnmovableDataStore {
  UnmovableData data_;
 public:
  void setData(const UnmovableData& data) { data_ = data; }
};

и передать по ссылке на const. Когда я использую это:

  std::cout << "UnmovableDataStore test:\n";
  UnmovableData umd;
  UnmovableDataStore umds;
  umds.setData(umd);

Я получаю вывод:

UnmovableDataStore test:
  copy assignment

только с одной копией, как и следовало ожидать.

Хранение некопируемого класса

Вы также можете иметь подвижный, но не копируемый класс:

class UncopyableData {
 public:
  UncopyableData() { } 
  UncopyableData(UncopyableData&& data) { std::cout << "  move constructor\n";}
  UncopyableData& operator=(UncopyableData&& data) { std::cout << "  move assignment\n"; return *this;}    
};

std::unique_ptr является примером подвижного, но не копируемого класса. В этом случае я бы, вероятно, написал бы класс для его хранения следующим образом:

class UncopyableDataStore {
  UncopyableData data_;
 public:
  void setData(UncopyableData&& data) { data_ = std::move(data); }
};

где я передаю rvalue ссылку и использую ее так:

  std::cout << "UncopyableDataStore test:\n";
  UncopyableData ucd;
  UncopyableDataStore ucds;
  ucds.setData(std::move(ucd));

со следующим выводом:

UncopyableDataStore test:
  move assignment

и обратите внимание, у нас теперь есть только один ход, который хорош.

Универсальные контейнеры

Контейнеры STL, однако, должны быть общими, они должны работать со всеми типами классов и быть максимально оптимальными. И если вам действительно нужна общая реализация хранилищ данных выше, это может выглядеть так:

template<class D>
class GenericDataStore {
  D data_;
 public:
  void setData(const D& data) { data_ = data; }
  void setData(D&& data) { data_ = std::move(data); }   
};

Таким образом, мы получаем максимально возможную производительность, независимо от того, используем мы классы, которые нельзя копировать или перемещать, но мы должны иметь как минимум две перегрузки метода setData которые могут привести к дублированию кода. Использование:

  std::cout << "GenericDataStore<Data> test:\n";
  Data d3;
  GenericDataStore<Data> gds;
  gds.setData(d3);

  std::cout << "GenericDataStore<UnmovableData> test:\n";
  UnmovableData umd2;
  GenericDataStore<UnmovableData> gds3;
  gds3.setData(umd2); 

  std::cout << "GenericDataStore<UncopyableData> test:\n";
  UncopyableData ucd2;
  GenericDataStore<UncopyableData> gds2;
  gds2.setData(std::move(ucd2));

Выход:

GenericDataStore<Data> test:
  copy assignment
GenericDataStore<UnmovableData> test:
  copy assignment
GenericDataStore<UncopyableData> test:
  move assignment

Живая демоверсия. Надеюсь, это поможет.