Подтвердить что ты не робот

Как разрешить построение конструирования для классов С++ (а не только структуры POD C)

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

#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() );
}

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

Macro make b
Не макрос сделать b
Переместить

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

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

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

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

N3290 Final С++ 0x черновик стр. 284:

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

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

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

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

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

Мой ответ:

Эта конструкция позволяет копировать копии для не-POD-типов. Я получил эту идею от ответа Дэвида Родригеса ниже. Это требует С++ 11 lambdas. В этом примере ниже я изменил 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 с шаблонами выражений
Устранение ненужных копий при создании составных объектов

4b9b3361

Ответ 1

Как уже упоминал Энтони, стандарт запрещает копирование элиции из аргумента функции возврату той же функции. Обоснованием, которое управляет этим решением, является то, что копирование elision (и перемещение 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();, то обе копии могут быть удалены: нет ограничений в отношении того, где tmp должен быть создан (он не является входным или выходным параметром функции, поэтому компилятор может перемещать его в любом месте, включая местоположение возвращаемого типа, и на вызывающей стороне, x может, как и в предыдущем примере, быть осторожно расположенным в местоположении тот же оператор return, который в основном означает, что tmp, оператор return и x может быть единственным объектом.

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

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

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

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

12,8/31

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

Ответ 2

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

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

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

Ответ 3

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

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

Ответ 4

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

Вместо:

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); } 

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