C++
Копирование Elision
Поиск…
Цель копирования
В стандарте есть места, где объект копируется или перемещается для инициализации объекта. Копирование elision (иногда называемое оптимизацией возвращаемого значения) - это оптимизация, при которой при определенных обстоятельствах компилятору разрешается избегать копирования или перемещения, даже если стандарт говорит, что это должно произойти.
Рассмотрим следующую функцию:
std::string get_string()
{
return std::string("I am a string.");
}
Согласно строгой формулировке стандарта, эта функция инициализирует временную std::string
, затем скопирует / переместит ее в объект возвращаемого значения, а затем уничтожит временную. В стандарте очень ясно, что так интерпретируется код.
Копирование elision - это правило, которое позволяет компилятору C ++ игнорировать создание временного и последующего копирования / уничтожения. То есть, компилятор может взять инициализирующее выражение для временного и инициализировать возвращаемое значение функции из него напрямую. Это, очевидно, экономит производительность.
Однако он имеет два видимых эффекта для пользователя:
Тип должен иметь конструктор copy / move, который был бы вызван. Даже если компилятор возвращает копию / перемещение, тип все равно должен быть скопирован / перемещен.
Побочные эффекты конструкторов копирования / перемещения не гарантируются в обстоятельствах, когда может возникнуть конфликт. Рассмотрим следующее:
struct my_type
{
my_type() = default;
my_type(const my_type &) {std::cout <<"Copying\n";}
my_type(my_type &&) {std::cout <<"Moving\n";}
};
my_type func()
{
return my_type();
}
Что вызовет func
? Ну, он никогда не будет печатать «Копирование», так как временный - это rvalue, а my_type
- подвижный. Так будет ли он печатать «Перемещение»?
Без правила elise для копирования это всегда должно было печатать «Moving». Но поскольку существует правило элиминации копирования, конструктор перемещения может быть вызван или не может быть вызван; это зависит от реализации.
И поэтому вы не можете зависеть от вызова конструкторов copy / move в контекстах, где возможна элиция.
Поскольку elision - это оптимизация, ваш компилятор может не поддерживать elision во всех случаях. И независимо от того, будет ли компилятор выдавать конкретный случай или нет, тип все равно должен поддерживать отмененную операцию. Поэтому, если построена копия, тип все равно должен иметь конструктор копирования, даже если он не будет вызываться.
Гарантированное копирование
Обычно элиция - это оптимизация. Несмотря на то, что практически каждая копия поддержки поддерживает копирование в простейшем случае, наличие элиции по-прежнему создает особую нагрузку на пользователей. А именно, тип, который копирует / перемещается, должен все же иметь операцию копирования / перемещения, которая была отменена.
Например:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
return std::lock_guard<std::mutex>(a_mutex);
}
Это может быть полезно в случаях, когда a_mutex
- это мьютекс, который находится в частной собственности какой-либо системы, но внешний пользователь может захотеть заблокировать его.
Это также не является законным, потому что std::lock_guard
не может быть скопирован или перемещен. Даже несмотря на то, что практически каждый компилятор C ++ будет удалять копию / перемещение, стандарт по-прежнему требует, чтобы тип имел эту операцию.
До C ++ 17.
C ++ 17 разрешает элицию путем эффективного переопределения самого значения определенных выражений, чтобы не происходило копирование / перемещение. Рассмотрим приведенный выше код.
В соответствии с формулировкой pre-C ++ 17 этот код говорит о создании временного и затем использует временное копирование / перемещение в возвращаемое значение, но временную копию можно удалить. В соответствии с формулировкой C ++ 17, которая не создает временного вообще.
В C ++ 17 любое выражение prvalue , когда используется для инициализации объекта того же типа, что и выражение, не создает временного. Выражение непосредственно инициализирует этот объект. Если вы вернете prvalue того же типа, что и возвращаемое значение, тогда тип не должен иметь конструктор copy / move. И поэтому в соответствии с правилами C ++ 17 приведенный выше код может работать.
Формулировка C ++ 17 работает в тех случаях, когда тип prvalue соответствует инициализированному типу. Поэтому, учитывая get_lock
выше, это также не потребует копирования / перемещения:
std::lock_guard the_lock = get_lock();
Поскольку результатом get_lock
является выражение prvalue, используемое для инициализации объекта того же типа, копирование или перемещение не произойдет. Это выражение никогда не создает временного; он используется для непосредственной инициализации the_lock
. Элис не существует, потому что копия / перемещение не будет устранено.
Поэтому термин «гарантированное копирование» является чем-то неправильным, но это название функции, как это предлагается для стандартизации C ++ 17 . Это не гарантирует никакого решения; он полностью исключает копирование / перемещение, переопределяя C ++, чтобы никогда не удалялось копирование / перемещение.
Эта функция работает только в случаях, связанных с выражением prvalue. Таким образом, это использует обычные правила:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
std::lock_guard<std::mutex> my_lock(a_mutex);
//Do stuff
return my_lock;
}
Несмотря на то, что это правильный пример для копирования, правила C ++ 17 не исключают копирование / перемещение в этом случае. Таким образом, тип должен иметь конструктор копирования / перемещения, который будет использоваться для инициализации возвращаемого значения. И поскольку lock_guard
этого не делает, это все еще ошибка компиляции. Реализациям разрешено отказывать в выдаче копий при передаче или возврате объекта с возможностью тривиально-копируемого типа. Это позволяет перемещать такие объекты в регистры, которые некоторые ABI могут выполнять в своих соглашениях о вызовах.
struct trivially_copyable {
int a;
};
void foo (trivially_copyable a) {}
foo(trivially_copyable{}); //copy elision not mandated
Возвращаемое значение elision
Если вы возвращаете выражение prvalue из функции, а выражение prvalue имеет тот же тип, что и возвращаемый тип функции, тогда копия из временного значения prvalue может быть удалена:
std::string func()
{
return std::string("foo");
}
Практически все компиляторы в этом случае будут исключать временную конструкцию.
Параметр elision
Когда вы передаете аргумент функции, а аргумент является выражением prvalue типа параметра функции, и этот тип не является ссылкой, то конструкцию prvalue можно удалить.
void func(std::string str) { ... }
func(std::string("foo"));
Это говорит о создании временной string
, а затем переместить ее в параметр функции str
. Копирование elision позволяет этому выражению напрямую создавать объект в str
, а не использовать временный + перемещение.
Это полезная оптимизация для случаев, когда конструктор объявляется explicit
. Например, мы могли бы написать выше как func("foo")
, но только потому, что string
имеет неявный конструктор, который преобразует из const char*
в string
. Если этот конструктор был explicit
, мы были бы вынуждены использовать временный вызов explicit
конструктора. Копирование elision избавляет нас от необходимости делать ненужную копию / перемещение.
Именованное возвращаемое значение elision
Если вы возвращаете выражение lvalue из функции, и это lvalue:
- представляет собой автоматическую переменную, локальную для этой функции, которая будет уничтожена после
return
- автоматическая переменная не является параметром функции
- и тип переменной такой же, как тип возвращаемого значения функции
Если все это так, то копирование / перемещение из lvalue можно отбросить:
std::string func()
{
std::string str("foo");
//Do stuff
return str;
}
Более сложные случаи имеют право на элицию, но чем более сложный случай, тем меньше вероятность того, что компилятор действительно сможет его ликвидировать:
std::string func()
{
std::string ret("foo");
if(some_condition)
{
return "bar";
}
return ret;
}
Компилятор все еще может игнорировать ret
, но шансы на их делают так идти вниз.
Как отмечалось ранее, для значений параметров не разрешено исключение.
std::string func(std::string str)
{
str.assign("foo");
//Do stuff
return str; //No elision possible
}
Копирование инициализации
Если вы используете выражение prvalue для копирования инициализации переменной, и эта переменная имеет тот же тип, что и выражение prvalue, тогда копирование может быть отменено.
std::string str = std::string("foo");
Инициализация кода эффективно преобразует это в std::string str("foo");
(есть незначительные различия).
Это также работает с возвращаемыми значениями:
std::string func()
{
return std::string("foo");
}
std::string str = func();
Без копирования elision это вызовет 2 обращения к конструктору перемещения std::string
. Копирование elision позволяет это вызвать конструктор перемещения 1 или нулевое время, и большинство компиляторов выберет последнее.