Я вважаю, у вас правильне спостереження, але неправильне тлумачення!
Копія не відбудеться, повертаючи значення, оскільки кожен звичайний розумний компілятор буде використовувати (N) RVO в цьому випадку. З C ++ 17 це обов'язково, тому ви не можете побачити жодну копію, повертаючи локальний згенерований вектор з функції.
Гаразд, давайте трохи пограємо з std::vector
тим, що буде під час будівництва або заповнивши його поетапно.
Перш за все, давайте може генерувати тип даних, який робить кожну копію чи рух видимим, як цей:
template <typename DATA >
struct VisibleCopy
{
private:
DATA data;
public:
VisibleCopy( const DATA& data_ ): data{ data_ }
{
std::cout << "Construct " << data << std::endl;
}
VisibleCopy( const VisibleCopy& other ): data{ other.data }
{
std::cout << "Copy " << data << std::endl;
}
VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
{
std::cout << "Move " << data << std::endl;
}
VisibleCopy& operator=( const VisibleCopy& other )
{
data = other.data;
std::cout << "copy assign " << data << std::endl;
}
VisibleCopy& operator=( VisibleCopy&& other ) noexcept
{
data = std::move( other.data );
std::cout << "move assign " << data << std::endl;
}
DATA Get() const { return data; }
};
А тепер давайте розпочнемо кілька експериментів:
using T = std::vector< VisibleCopy<int> >;
T Get1()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
std::cout << "End init" << std::endl;
return vec;
}
T Get2()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec(4,0);
std::cout << "End init" << std::endl;
return vec;
}
T Get3()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
T Get4()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.reserve(4);
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
int main()
{
auto vec1 = Get1();
auto vec2 = Get2();
auto vec3 = Get3();
auto vec4 = Get4();
// All data as expected? Lets check:
for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}
Що ми можемо спостерігати:
Приклад 1) Ми створюємо вектор зі списку ініціалізатора, і, можливо, ми очікуємо, що ми побачимо 4 рази побудувати і 4 ходи. Але ми отримуємо 4 екземпляри! Це звучить трохи загадково, але причина - реалізація списку ініціалізаторів! Просто не дозволяється переміщатися зі списку, оскільки ітератор зі списку - const T*
це неможливе переміщення елементів із нього. Детальну відповідь на цю тему можна знайти тут: Initilizer_list та семантика переміщення
Приклад 2) У цьому випадку ми отримуємо початкову конструкцію та 4 копії значення. Це не є особливим, і ми можемо очікувати.
Приклад 3) Також тут ми будуємо конструкцію та деякі кроки, як очікувалося. З моєю реалізацією stl вектор щоразу зростає на коефіцієнт 2. Отже, ми бачимо першу конструкцію, іншу і тому, що вектор змінює розмір від 1 до 2, ми бачимо переміщення першого елемента. Додаючи 3, ми бачимо розмір від 2 до 4, який потребує переміщення перших двох елементів. Все як очікувалося!
Приклад 4) Тепер ми резервуємо простір і заповнюємо пізніше. Тепер у нас немає жодної копії і жодного руху!
У всіх випадках ми не бачимо жодного переміщення чи копіювання, повертаючи вектор назад абоненту! (N) RVO відбувається, і подальших дій на цьому кроці не потрібно!
Назад до свого питання:
"Як знайти C ++ операції копіювання копій"
Як було показано вище, ви можете ввести клас проксі між ними для налагодження.
Зробити приватний копіювальний механізм може не працювати у багатьох випадках, оскільки у вас можуть бути потрібні копії та деякі приховані. Як вище, тільки код, наприклад, 4, буде працювати з приватним копіратором! І я не можу відповісти на запитання, якщо приклад 4 - найшвидший, оскільки миром ми наповнюємо мир.
Вибачте, що тут не можу запропонувати загального рішення для пошуку "небажаних" копій. Навіть якщо ви копаєте свій код для дзвінків memcpy
, ви не знайдете все, що також memcpy
буде оптимізовано, і ви побачите безпосередньо деякі інструкції асемблера, які виконують роботу, без виклику вашої бібліотечної memcpy
функції.
Мій натяк - не зосереджуватись на такій незначній проблемі. Якщо у вас є реальні проблеми з продуктивністю, візьміть профайлер і заміряйте. Існує так багато потенційних вбивць продуктивності, що інвестувати багато часу на помилкове memcpy
використання здається не такою гідною ідеєю.
std::vector
будь-якими способами не є тим, яким він має намір бути . У вашому прикладі показана явна копія, і застосуватиstd::move
функцію так, як ви самі пропонуєте, є природним і правильним підходом, (знову ж таки імхо), якщо копія - це не те, що потрібно. Зауважте, що деякі компілятори можуть пропустити копіювання, якщо включені прапори оптимізації, а вектор не змінюється.