Предположим, у меня есть структура и я пытаюсь определить метод, который мог бы:
Я бы подумал, что в реализации я хочу определить метод inplace, а затем определить обычный метод через прокси. В качестве примера давайте рассмотрим простую структуру, которая имеет член int num и может add добавить к нему еще один int. Я могу представить это кодирование двумя разными способами: либо add_inplace будет возвратом void, либо add_inplace return *this. Вот две реализации:
struct myStruct_impl1{
int num = 0;
void add_inplace(const int& other)
{
num += other;
return;
}
myStruct_impl1 add(const int& other) const
{
myStruct_impl1 tmp(*this);
tmp.add_inplace(other);
return tmp;
}
};
struct myStruct_impl2{
int num = 0;
myStruct_impl2& add_inplace(const int& other)
{
num += other;
return *this;
}
myStruct_impl2 add(const int& other) const
{
return myStruct_impl2(*this).add_inplace(other);
}
};
Сейчас я думаю о том, что в методе myStruct_impl1add возвращается именованный tmp, поэтому есть возможность выполнить NRVO компилятором (хотя это и не гарантировано). С другой стороны, в myStruct_impl2 метод add возвращается после возврата ссылки на lvalue (возврат add_inplace). Что происходит сейчас? Считается ли это временным и, возможно, будет выполнено RVO (гарантировано после C++17)? Я думаю об этом частично неправильно? Я делаю все это неправильно, и есть ли лучший способ подумать о том, чтобы методы, определенные прокси-сервером, были внедрены в методы?
******** ОБНОВЛЯТЬ *********
Как предложил @zerocukor287 в комментариях, я создал конструкторы и назначения копирования/перемещения, а деструктор печатает их при вызове. Вот код:
#include <cstdlib>
#include <iostream>
struct myStruct_impl1{
int num;
myStruct_impl1() : num(0) {};
myStruct_impl1(const int& other) : num(other) {}
myStruct_impl1(const myStruct_impl1& other) : num(other.num) { std::cout << "called impl1 Ctor" << std::endl; }
myStruct_impl1(myStruct_impl1&& other) noexcept : num(std::move(other.num)) { std::cout << "called impl1 Mtor" << std::endl; }
myStruct_impl1& operator=(const myStruct_impl1& other) { std::cout << "called impl1 Cass" << std::endl; num = other.num; return *this; }
myStruct_impl1& operator=(myStruct_impl1&& other) noexcept { std::cout << "called impl1 Mass" << std::endl; num = std::move(other.num); return *this; }
~myStruct_impl1() { std::cout << "called impl1 Dest" << std::endl; }
void add_inplace(const int& other)
{
num += other;
return;
}
myStruct_impl1 add(const int& other) const
{
myStruct_impl1 tmp(*this);
tmp.add_inplace(other);
return tmp;
}
};
struct myStruct_impl2{
int num;
myStruct_impl2() : num(0) {};
myStruct_impl2(const int& other) : num(other) {}
myStruct_impl2(const myStruct_impl2& other) : num(other.num) { std::cout << "called impl2 Ctor" << std::endl; }
myStruct_impl2(myStruct_impl2&& other) noexcept : num(std::move(other.num)) { std::cout << "called impl2 Mtor" << std::endl; }
myStruct_impl2& operator=(const myStruct_impl2& other) { std::cout << "called impl2 Cass" << std::endl; num = other.num; return *this; }
myStruct_impl2& operator=(myStruct_impl2&& other) noexcept { std::cout << "called impl2 Mass" << std::endl; num = std::move(other.num); return *this; }
~myStruct_impl2() { std::cout << "called impl2 Dest" << std::endl; }
myStruct_impl2& add_inplace(const int& other)
{
num += other;
return *this;
}
myStruct_impl2 add(const int& other) const
{
return myStruct_impl2(*this).add_inplace(other);
}
};
int main(int argc, char* argv[]){
myStruct_impl1 ex1(10);
myStruct_impl2 ex2(10);
int y = 42;
std::cout << "----------- Impl 1 -----------" << std::endl;
ex1.add(y);
std::cout << "----------- Impl 2 -----------" << std::endl;
ex2.add(y);
std::cout << "----------- END ------------" << std::endl;
return 0;
}
Код был скомпилирован с использованием g++ 11.4.0 и g++ 13.1.0, с std=c++11,17,20, а также с -O3 и без него. Во всех случаях результат был следующий:
----------- Impl 1 -----------
called impl1 Ctor
called impl1 Dest
----------- Impl 2 -----------
called impl2 Ctor
called impl2 Ctor
called impl2 Dest
called impl2 Dest
----------- END ------------
called impl2 Dest
called impl1 Dest
Таким образом, возврат из возврата ссылки lvalue в Impl2 вызывает два вызова ctor, и его, вероятно, следует избегать в подобных случаях. Однако @Marek R предоставил в комментариях фрагмент кода, где сборка выглядит одинаково в обоих случаях? Я не уверен, как рассуждать о том, что делает код.
Вы можете проверить это самостоятельно, создав определяемые пользователем конструкторы копирования и перемещения, которые, скажем, записывают в std::cout. Затем создайте несколько вариантов использования и посмотрите, что происходит.
Возврат по ссылке lvalue не требует каких-либо rvo/nrvo и т. д. на самом деле.
Для компилятора существенной разницы нет: godbolt.org/z/3nPd6xGfP





Сейчас я думаю о том, что в методе
myStruct_impl1add возвращается именованныйtmp, поэтому есть возможность выполнить NRVO компилятором (хотя это и не гарантировано).
Точно.
С другой стороны, в
myStruct_impl2методaddвозвращается после возврата ссылки на lvalue (возвратadd_inplace). Что происходит сейчас? Считается ли это временным и, возможно, будет выполнено RVO (гарантировано после C++17)?
Ссылка не является временной.
и RVO не будет применяться к myStruct_impl2(*this).
Ваше утверждение return будет эквивалентно
return myStruct_impl2(myStruct_impl2(*this).add_inplace(other));
Таким образом, будет вызван конструктор копирования.
Внешнее/дополнительное временное не будет материализовано.
Реализация из myStruct_impl1 является наиболее оптимизированным способом (даже если NRVO не применяется, у вас будет дополнительный ход вместо дополнительной копии), если вы повторно используете метод add.
Оптимальным способом было бы переписать без повторного использования другого метода:
myStruct_impl3 add(const int& other) const
{
return myStruct_impl3{this->num + other.num};
}
Однако @Marek R предоставил в комментариях фрагмент кода, где сборка выглядит одинаково в обоих случаях? Я не уверен, как рассуждать о том, что делает код.
Компилятор может выполнить другие оптимизации, следуя правилу «как если бы». Вывод не требуется, просто элемент int для изменения. Лишнюю копию можно удалить.
Возможно, вы даже увидите, что add_inplace даже не называется.
Похоже на микрооптимизацию.