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

Правильное использование ссылок rvalue в качестве параметров

В качестве примера возьмем следующий метод:

void Asset::Load( const std::string& Path )
{
    // complicated method....
}

Общее использование этого метода будет следующим:

Asset ExampleAsset;
ExampleAsset.Load("image0.png");

Поскольку мы знаем большую часть времени, когда путь является временным rvalue, имеет смысл добавить Rvalue-версию этого метода? И если да, то это правильная реализация;

void Asset::Load( const std::string& Path )
{
    // complicated method....
}
void Asset::Load( std::string&& Path )
{
     Load(Path); // call the above method
}

Это правильный подход к написанию версий методов rvalue?

4b9b3361

Ответ 1

В вашем конкретном случае вторая перегрузка бесполезна.

С исходным кодом, который имеет только одну перегрузку для Load, эта функция вызывается для lvalues ​​и rvalues.

С новым кодом первая перегрузка вызывается для lvalues, а вторая вызывается для rvalues. Однако вторая перегрузка вызывает первую. В конце, эффект вызова того или другого подразумевает, что будет выполняться одна и та же операция (независимо от первой перегрузки).

Следовательно, эффекты исходного кода и нового кода одинаковы, но первый код просто проще.

Решая, должна ли функция принимать аргумент по значению, lvalue reference или rvalue reference сильно зависит от того, что он делает. Вы должны предоставить перегрузку, берущую ссылки rvalue, когда вы хотите переместить переданный аргумент. Есть несколько хороших ссылок на перемещение semantincs там, поэтому я не буду здесь его описывать.

Bonus

Чтобы помочь мне задуматься над этим простым классом probe:

struct probe {
    probe(const char*  ) { std::cout << "ctr " << std::endl; }
    probe(const probe& ) { std::cout << "copy" << std::endl; }
    probe(probe&&      ) { std::cout << "move" << std::endl; }
};

Теперь рассмотрим эту функцию:

void f(const probe& p) {
    probe q(p);
    // use q;
}

Вызов f("foo"); вызывает следующий вывод:

ctr
copy

Никаких сюрпризов здесь: мы создаем временный probe, передающий const char* "foo". Отсюда первая выходная линия. Затем это временное значение привязано к p, а внутри f создается копия q of p. Следовательно, вторая выходная линия.

Теперь рассмотрим выбор p по значению, т.е. изменим f на:

void f(probe p) {
    // use p;
}

Вывод f("foo"); теперь

ctr

Некоторые удивятся, что в этом случае нет копии! В общем случае, если вы принимаете аргумент по ссылке и копируете его внутри своей функции, тогда лучше принять аргумент по значению. В этом случае вместо создания временного и копирования его компилятор может построить аргумент (p в этом случае) непосредственно из ввода ("foo"). Для получения дополнительной информации см. Хотите скорость? Pass by Value. от Дейва Абрахама.

В этом руководстве есть два примечательных исключения: конструкторы и операторы присваивания.

Рассмотрим этот класс:

struct foo {
    probe p;
    foo(const probe& q) : p(q) { }
};

Конструктор принимает probe по ссылке const, а затем копирует его в p. В этом случае, следуя приведенному выше руководству, не приносит никакого улучшения производительности, и конструктор копирования probe будет вызван в любом случае. Однако при принятии q по значению может возникнуть проблема с разрешением перегрузки, аналогичная той, с которой выполняется оператор присваивания, который я сейчас рассмотрю.

Предположим, что наш класс probe имеет метод небрасывания swap. Тогда предлагаемая реализация его оператора присваивания (в настоящее время рассматривается в терминах С++ 03)

probe& operator =(const probe& other) {
    probe tmp(other);
    swap(tmp);
    return *this;
}

Затем, согласно вышеприведенному руководству, лучше написать его вот так

probe& operator =(probe tmp) {
    swap(tmp);
    return *this;
}

Теперь введите С++ 11 с ссылками rvalue и переместите семантику. Вы решили добавить оператор присваивания перемещения:

probe& operator =(probe&&);

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

Собственно, эта проблема не относится к конструкторам и операторам присваивания и может случиться с любой функцией. (Скорее всего, вы столкнетесь с конструкторами и операторами присваивания.) Например, вызов g("foo");, когда g имеет следующие две перегрузки, повышает двусмысленность:

void g(probe);
void g(probe&&);

Ответ 2

Если вы не делаете что-то другое, кроме вызова ссылочной версии lvalue Load, вам не нужна вторая функция, так как rvalue будет привязываться к ссылке const lvalue.

Ответ 3

В общем случае вопрос о том, будет ли внутри вас делать копию (явно или неявно) входящего объекта (указать аргумент T&&), или вы просто его используете (придерживайтесь [const] T&).

Ответ 4

Поскольку мы знаем большую часть времени, когда путь является временным rvalue, имеет смысл добавить Rvalue-версию этого метода?

Вероятно, нет... Если вам не нужно делать что-то сложное внутри Load(), для которого требуется неконстантный параметр. Например, возможно, вы хотите std::move(Path) в другой поток. В этом случае имеет смысл использовать семантику перемещения.

Это правильный подход к написанию версий методов rvalue?

Нет, вы должны сделать это наоборот:

void Asset::load( const std::string& path )
{
     auto path_copy = path;
     load(std::move(path_copy)); // call the below method
}
void Asset::load( std::string&& path )
{
    // complicated method....
}

Ответ 5

Если ваша функция-член Load не назначается из входящей строки, вы должны просто предоставить void Asset::Load(const std::string& Path).

Если вы назначаете из входящего path, скажите переменной-члену, то есть сценарий, где было бы немного более эффективно предоставлять void Asset::Load(std::string&& Path) тоже, но вам понадобится другая реализация, которая назначает ala loaded_from_path_ = std::move(Path);.

Потенциальная выгода заключается в том, что при версии && они могут получить область свободного хранилища, принадлежащую переменной-члену, избегая пессимистического delete[] иона этого буфера внутри void Asset::Load(const std::string& Path) и возможное перераспределение в следующий раз, когда назначается строка вызывающего абонента (предполагая, что буфер достаточно велик, чтобы соответствовать его следующему значению).

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