Как разрешить копирование elision для классов C ++ (не только для структур POD C) - PullRequest
27 голосов
/ 04 мая 2011

Рассмотрим следующий код:

#include <iostream>
#include <type_traits>

struct A
{
  A() {}
  A(const A&) { std::cout << "Copy" << std::endl; }
  A(A&&) { std::cout << "Move" << std::endl; }
};

template <class T>
struct B
{
  T x;
};

#define MAKE_B(x) B<decltype(x)>{ x }

template <class T>
B<T> make_b(T&& x)
{
  return B<T> { std::forward<T>(x) };
}

int main()
{
  std::cout << "Macro make b" << std::endl;
  auto b1 = MAKE_B( A() );
  std::cout << "Non-macro make b" << std::endl;
  auto b2 = make_b( A() );
}

Это выводит следующее:

Макросъемка B
Немакро-марка б
Переместить

Обратите внимание, что b1 строится без движения, но для построения b2 требуется ход.

Мне также нужно ввести вывод, поскольку A в реальной жизни может быть сложным типом, который сложно написать явно. Мне также нужно иметь возможность вкладывать вызовы (т. Е. make_c(make_b(A()))).

Возможна ли такая функция?

Дальнейшие мысли:

N3290 Final C ++ 0x черновик 284:

Это исключение операций копирования / перемещения, называется копия разрешения, разрешено в следующие обстоятельства:

когда временный объект класса , который имеет не привязан к ссылке (12.2) будет скопирован / перемещен в класс объект с тем же CV-неквалифицированным типа, операция копирования / перемещения может быть опущено при построении временного объект прямо в цель опущено копирование / перемещение

К сожалению, кажется, что мы не можем исключить копии (и перемещения) параметров функции для получения результатов функции (включая конструкторы), поскольку эти временные привязки либо привязаны к ссылке (при передаче по ссылке), либо больше не являются временными (при передаче через значение). Кажется, единственный способ исключить все копии при создании составного объекта - это создать его как совокупность. Однако агрегаты имеют определенные ограничения, такие как требование, чтобы все члены были открытыми, и не определяемые пользователем конструкторы.

Я не думаю, что для C ++ имеет смысл разрешать оптимизацию для составного построения C-структур POD, но не разрешать такие же оптимизации для конструирования класса C ++, не связанного с POD.

Есть ли способ разрешить копирование / перемещение elision для неагрегированного построения?

Мой ответ:

Эта конструкция позволяет исключать копии для типов, отличных от POD. Я получил эту идею из ответа Дэвида Родригеса ниже. Требуется лямбда C ++ 11. В этом примере ниже я изменил make_b, чтобы принять два аргумента, чтобы сделать вещи менее тривиальными Нет никаких вызовов для перемещения или копирования конструкторов.

#include <iostream>
#include <type_traits>

struct A
{
  A() {}
  A(const A&) { std::cout << "Copy" << std::endl; }
  A(A&&) { std::cout << "Move" << std::endl; }
};

template <class T>
class B
{
public:
  template <class LAMBDA1, class LAMBDA2>
  B(const LAMBDA1& f1, const LAMBDA2& f2) : x1(f1()), x2(f2()) 
  { 
    std::cout 
    << "I'm a non-trivial, therefore not a POD.\n" 
    << "I also have private data members, so definitely not a POD!\n";
  }
private:
  T x1;
  T x2;
};

#define DELAY(x) [&]{ return x; }

#define MAKE_B(x1, x2) make_b(DELAY(x1), DELAY(x2))

template <class LAMBDA1, class LAMBDA2>
auto make_b(const LAMBDA1& f1, const LAMBDA2& f2) -> B<decltype(f1())>
{
  return B<decltype(f1())>( f1, f2 );
}

int main()
{
  auto b1 = MAKE_B( A(), A() );
}

Если кто-нибудь знает, как добиться этого более аккуратно, мне было бы интересно увидеть это.

Предыдущее обсуждение:

Это несколько следует из ответов на следующие вопросы:

Можно ли оптимизировать создание составных объектов из временных?
Избежание необходимости #define с шаблонами выражений
Устранение ненужных копий при построении составных объектов

Ответы [ 4 ]

8 голосов
/ 21 мая 2011

Как уже упоминал Энтони, стандарт запрещает копировать elision из аргумента функции в возвращение той же самой функции. Логическое обоснование этого решения заключается в том, что удаление копии (и перемещение перемещения) - это оптимизация, с помощью которой два объекта в программе объединяются в одну и ту же область памяти, то есть копия удаляется, если оба объекта являются одним. Ниже приведена (частичная) стандартная цитата, за которой следует ряд обстоятельств, при которых допускается копирование, которые не включают этот конкретный случай.

Так чем же отличается этот конкретный случай? Различие состоит в основном в том, что тот факт, что между исходным и скопированным объектами есть вызов функции, и вызов функции подразумевает наличие дополнительных ограничений, в частности, соглашения о вызовах.

Учитывая функцию T foo( T ) и пользователя, вызывающего T x = foo( T(param) );, в общем случае с отдельной компиляцией, компилятор создаст объект $tmp1 в месте, в котором соглашение о вызовах требует, чтобы первый аргумент был. Затем он вызовет функцию и инициализирует x из оператора return. Вот первая возможность удаления копии: аккуратно поместив x в место, где находится возвращенный временный объект, x и возвращенный объект из foo становятся единым объектом, и эта копия удаляется. Все идет нормально. Проблема в том, что соглашение о вызовах в общем случае не будет иметь возвращенный объект и параметр в одном и том же месте, и поэтому $tmp1 и x не могут быть одним местом в памяти.

Не видя определения функции, компилятор не может знать, что единственная цель аргумента функции - служить оператором возврата, и поэтому он не может исключить эту дополнительную копию. Можно утверждать, что если функция inline, то у компилятора будет отсутствующая дополнительная информация, чтобы понять, что временное значение, используемое для вызова функции, возвращаемое значение и x представляют собой один объект. Проблема заключается в том, что эту конкретную копию можно удалить только в том случае, если код действительно встроенный (не только если он помечен как inline, но фактически встроен ) Если требуется вызов функции, то копия не может быть опущены. Если бы стандарт позволял исключать эту копию при вставке кода, это означало бы, что поведение программы будет отличаться из-за компилятора, а не из-за кода пользователя - ключевое слово inline не приводит к принудительному встраиванию, оно только означает, что множественные определения одной и той же функции не представляют собой нарушение ODR.

Обратите внимание, что если переменная создана внутри функции (по сравнению с переданной в нее), как в: T foo() { T tmp; ...; return tmp; } T x = foo();, то обе копии могут быть исключены: по состоянию на * 1027 ограничения нет должен быть создан (это не входной или выходной параметр функции, поэтому компилятор может переместить его куда угодно, включая местоположение возвращаемого типа, и на вызывающей стороне x может быть, как в предыдущем примере, аккуратно расположен в том же самом операторе возврата, что в основном означает, что tmp, оператор возврата и x могут быть одним объектом.

Что касается вашей конкретной проблемы, если вы прибегаете к макросу, код встроен, нет никаких ограничений на объекты, и копия может быть удалена. Но если вы добавите функцию, вы не сможете удалить копию из аргумента в оператор return. Так что просто избегайте этого. Вместо использования шаблона, который будет перемещать объект, создайте шаблон, который будет создавать объект:

template <typename T, typename... Args>
T create( Args... x ) {
   return T( x... );
}

И эта копия может быть удалена компилятором.

Обратите внимание, что я не имел дело со строительством ходов, так как вы, кажется, обеспокоены стоимостью строительства даже ходов, хотя я считаю, что вы лаете не на то дерево. Учитывая мотивирующий реальный сценарий использования, я вполне уверен, что люди здесь предложат пару эффективных идей.

12,8 / 31 * * тысяча сорок шесть

При соблюдении определенных критериев реализация может опустить конструкцию копирования / перемещения объекта класса, даже если конструктор копирования / перемещения и / или деструктор для объекта имеют побочные эффекты. В таких случаях реализация рассматривает источник и цель пропущенной операции копирования / перемещения просто как два разных способа обращения к одному и тому же объекту, и уничтожение этого объекта происходит в более поздние времена, когда два объекта были бы уничтожен без оптимизации.

5 голосов
/ 04 мая 2011

... но для построения b2 требуется ход.

Нет, это не так.Компилятору разрешено исключать ход;происходит ли это в зависимости от реализации, в зависимости от нескольких факторов.Также разрешено перемещаться, но оно не может копировать (в этом случае вместо копирования должно использоваться перемещение).

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

4 голосов
/ 06 мая 2011

Нельзя оптимизировать копирование / перемещение объекта A из параметра make_b в член созданного объекта B.

Однако в этом и заключается смысл семантики перемещения - предоставляя легкую операцию перемещения для A, вы можете избежать потенциально дорогой копии. например если A было на самом деле std::vector<int>, то копирование содержимого вектора можно избежать с помощью конструктора перемещения, и вместо этого будут передаваться только служебные указатели.

1 голос
/ 07 мая 2011

Это не большая проблема. Все, что ему нужно, это немного изменить структуру кода.

Вместо:

B<A> create(A &&a) { ... }
int main() { auto b = create(A()); }

Вы всегда можете сделать:

int main() { A a; B<A> b(a); ... }

Если конструктор B такой, то он не будет копировать:

template<class T>
class B { B(T &t) :t(t) { } T &t; };

Также будет работать составной корпус:

struct C { A a; B b; };
void init(C &c) { c.a = 10; c.b = 20; }
int main() { C c; init(c); } 

И для этого даже не нужны функции c ++ 0x.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...