Когда перегрузка проход по ссылке (l-значение и r-значение) предпочтительнее передачи по значению?

Я видел, что сказано, что operator= записанный для получения параметра одного и того же типа по значению служит как оператором копирования, так и оператором перемещения в C ++ 11:

Foo& operator=(Foo f)
{
swap(f);
return *this;
}

Где альтернатива будет более чем в два раза больше строк с большим количеством повторений кода и возможностью ошибки:

Foo& operator=(const Foo& f)
{
Foo f2(f);
swap(f2);
return *this;
}

Foo& operator=(Foo&& f)
{
Foo f2(std::move(f));
swap(f2);
return *this;
}

При каких обстоятельствах предпочтительнее перегрузка ref-to-const и r-value, чем
передать по значению или когда это необходимо? Я думаю о std::vector::push_back,
например, который определяется как две перегрузки:

void push_back (const value_type& val);
void push_back (value_type&& val);

Следуя первому примеру, где передать по значению служит копированием
оператор и оператор присваивания ходов
, не может push_back быть определенным в
Стандарт должен быть единой функцией?

void push_back (value_type val);

22

Решение

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

Этот класс управляет буфером динамического размера и поддерживает как capacity (максимальная длина, которую может содержать буфер), и size (текущая длина). Если vector оператор присваивания копии реализован swapтогда, несмотря ни на что, новый буфер всегда выделяется, если rhs.size() != 0,

Однако если lhs.capacity() >= rhs.size()нет необходимости выделять новый буфер. Можно просто назначить / построить элементы из rhs в lhs, Когда тип элемента тривиально копируемый, это может сводиться только к memcpy, Это может быть много, много быстрее, чем выделять и освобождать буфер.

Та же проблема для std::string,

Та же проблема для MyType когда MyType имеет члены данных, которые std::vector и / или std::string,

Есть только 2 раза, когда вы захотите реализовать назначение копирования с помощью swap:

  1. Вы знаете, что swap метод (включая обязательную конструкцию копии, когда rhs является lvalue) не будет ужасно неэффективным.

  2. Вы знаете, что вы будете всегда нужен оператор копирования копии, чтобы иметь строгую гарантию безопасности исключений.

Если вы не уверены в 2, другими словами, вы думаете, что оператор копирования может иногда нужна строгая гарантия безопасности исключений, не выполняйте назначение с точки зрения обмена. Ваши клиенты могут легко получить такую ​​же гарантию, если вы предоставите одно из:

  1. Нет, кроме своп.
  2. Нет, кроме оператора назначения перемещения.

Например:

template <class T>
T&
strong_assign(T& x, T y)
{
using std::swap;
swap(x, y);
return x;
}

или же:

template <class T>
T&
strong_assign(T& x, T y)
{
x = std::move(y);
return x;
}

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

На:

void push_back(const value_type& val);
void push_back(value_type&& val);

Представить vector<big_legacy_type> где:

class big_legacy_type
{
public:
big_legacy_type(const big_legacy_type&);  // expensive
// no move members ...
};

Если бы у нас было только:

void push_back(value_type val);

затем push_backЯ имею значение big_legacy_type в vector потребует 2 копии вместо 1, даже когда capacity было достаточно. Это было бы катастрофой, с точки зрения производительности.

Обновить

Вот HelloWorld, который вы сможете запустить на любой платформе, соответствующей C ++ 11:

#include <vector>
#include <random>
#include <chrono>
#include <iostream>

class X
{
std::vector<int> v_;
public:
explicit X(unsigned s) : v_(s) {}

#if SLOW_DOWN
X(const X&) = default;
X(X&&) = default;
X& operator=(X x)
{
v_.swap(x.v_);
return *this;
}
#endif
};

std::mt19937_64 eng;
std::uniform_int_distribution<unsigned> size(0, 1000);

std::chrono::high_resolution_clock::duration
test(X& x, const X& y)
{
auto t0 = std::chrono::high_resolution_clock::now();
x = y;
auto t1 = std::chrono::high_resolution_clock::now();
return t1-t0;
}

int
main()
{
const int N = 1000000;
typedef std::chrono::duration<double, std::nano> nano;
nano ns(0);
for (int i = 0; i < N; ++i)
{
X x1(size(eng));
X x2(size(eng));
ns += test(x1, x2);
}
ns /= N;
std::cout << ns.count() << "ns\n";
}

Я закодировал XКопировать оператор присваивания двумя способами:

  1. Неявно, что эквивалентно вызову vectorоператор присваивания копии.
  2. С идиомой копирования / обмена, предположительно под макросом SLOW_DOWN, Я думал о названии SLEEP_FOR_AWHILE, но этот способ на самом деле намного хуже, чем заявления сна, если вы используете устройство с питанием от батареи.

Тест строит несколько случайных размеров vector<int>s между 0 и 1000, и назначает их миллион раз. Каждый раз он умножает время, а затем находит среднее время в наносекундах с плавающей запятой и выводит его. Если два последовательных вызова ваших часов с высоким разрешением не возвращают что-то менее 100 наносекунд, вы можете увеличить длину векторов.

Вот мои результаты:

$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp
$ a.out
428.348ns
$ a.out
438.5ns
$ a.out
431.465ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp
$ a.out
617.045ns
$ a.out
616.964ns
$ a.out
618.808ns

Я вижу, что при использовании этого простого теста производительность при копировании / замене достигает 43%. YMMV.

Вышеуказанный тест в среднем обладает достаточной емкостью в половину времени. Если мы примем это в крайности:

  1. LHS имеет достаточную емкость все время.
  2. lhs не имеет достаточной мощности ни разу.

тогда преимущество в производительности при назначении копии по умолчанию над идиомой копирования / обмена варьируется от 560% до 0%. Идиома копирования / обмена никогда не бывает быстрее и может быть значительно медленнее (для этого теста).

Хотите скорость? Мера.

27

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

Других решений пока нет …

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