Я верю, что у вас правильное наблюдение, но неверное толкование!
Копирование не произойдет при возврате значения, потому что каждый нормальный умный компилятор будет использовать (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*
что делает невозможным перемещение элементов из него. Подробный ответ на эту тему можно найти здесь: initializer_list и семантика перемещения
Пример 2) В этом случае мы получаем начальную конструкцию и 4 копии значения. В этом нет ничего особенного, и это то, что мы можем ожидать.
Пример 3) Также здесь, мы строим и некоторые шаги, как ожидалось. С моей реализацией stl вектор растет в 2 раза каждый раз. Таким образом, мы видим первую конструкцию, другую, и поскольку вектор изменяется с 1 на 2, мы видим движение первого элемента. Добавляя 3, мы видим изменение размера от 2 до 4, которое требует перемещения первых двух элементов. Все как и ожидалось!
Пример 4) Теперь мы оставляем за собой место и заполняем позже. Теперь у нас нет копии и больше нет движения!
Во всех случаях мы не видим ни перемещения, ни копирования, вообще возвращая вектор вызывающей стороне! (N) RVO происходит, и на этом этапе никаких дальнейших действий не требуется!
Вернуться к вашему вопросу:
«Как найти поддельные операции копирования C ++»
Как видно выше, вы можете ввести прокси-класс между ними для целей отладки.
Приватное копирование ctor может не сработать во многих случаях, так как у вас могут быть некоторые требуемые копии и некоторые скрытые. Как и выше, только код для примера 4 будет работать с частным копи-ctor! И я не могу ответить на вопрос, является ли пример 4 самым быстрым, поскольку мы наполняем мир миром.
Извините, что я не могу предложить общее решение для поиска «нежелательных» копий здесь. Даже если вы копаете свой код для вызовов memcpy
, вы не найдете все, что также memcpy
будет оптимизировано, и вы увидите, как некоторые инструкции на ассемблере выполняют свою работу без вызова вашей библиотечной memcpy
функции.
Мой намек не в том, чтобы сосредоточиться на такой незначительной проблеме. Если у вас есть реальные проблемы с производительностью, возьмите профилировщик и измерьте. Потенциальных убийц производительности так много, что тратить много времени на ложное memcpy
использование кажется не очень полезной идеей.
std::vector
каким-либо образом не является тем, чем оно является . В вашем примере показана явная копия, и это вполне естественный и правильный подход (опять же imho) применитьstd::move
функцию, как вы предлагаете сами, если копия не то, что вам нужно. Обратите внимание, что некоторые компиляторы могут пропустить копирование, если флаги оптимизации включены, а вектор неизменен.