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

Я видел, что он сказал, что a operator=, записанный для принятия параметра того же типа по-значению, служит как оператором присваивания копии, так и оператором присваивания перемещения в С++ 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-const и r-значение предпочтительнее передавать по значению или когда это необходимо? Я думаю о std::vector::push_back, например, который определяется как две перегрузки:

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

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

void push_back (value_type val);

Ответ 1

Для типов, оператор копирования которых может перерабатывать ресурсы, замена с копией практически никогда не является лучшим способом реализации оператора присваивания копий. Например, посмотрите 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:

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

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

Если вы не уверены в 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;
}

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

О:

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 значение lvalue big_legacy_type в vector потребует 2 экземпляра вместо 1, даже если capacity было достаточно. Это было бы катастрофой, с точки зрения производительности.

Обновление

Вот HelloWorld, который вы можете запустить на любой совместимой платформе С++ 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 оператор назначения копирования двумя способами:

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

Тест конструирует некоторый случайный размер vector<int> между 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% -ный выигрыш в производительности для идиомы copy/swap с помощью этого простого теста. YMMV.

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

  • lhs имеет достаточную емкость все время.
  • lhs имеет достаточную пропускную способность в любое время.

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

Хотите скорость? Измерить.