Лучший способ написать генератор объектов для класса шаблона RAII?


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

Проблема, которую я предвижу, заключается в том, что вспомогательная функция, которая заботится о дедукции типа для меня, будет возвращать объект по значению, что приведет (**) к преждевременному вызову деструктора RAII, когда копия будет сделана. Возможно, семантика перемещения C++0x мог бы помочь, но это не вариант для меня.

Кто-нибудь видел эту проблему раньше и имеет хорошее решение?

Вот что у меня есть:

template<typename T, typename U, typename V>
class FooAdder
{
private:
  typedef OtherThing<T, U, V> Thing;
  Thing &thing_;
  int a_;
  // many other members
public:
  FooAdder(Thing &thing, int a);
  ~FooAdder();
  FooAdder &foo(T t, U u);
  FooAdder &bar(V v);
};

Суть в том, что OtherThing имеет ужасный интерфейс, и FooAdder, как предполагается, чтобы сделать его проще в использовании. Предполагаемое использование примерно таково:

FooAdder(myThing, 2)
  .foo(3, 4)
  .foo(5, 6)
  .bar(7)
  .foo(8, 9);

Конструктор FooAdder инициализирует некоторые внутренние структуры данных. Методы foo и bar заполняют эти структуры данных. Dtor ~FooAdder сворачивает все и вызывает a метод на thing_, заботясь обо всех гадостях.

Это было бы прекрасно, если бы FooAdder не было шаблоном. Но поскольку это так, мне нужно было бы ввести типы, более похожие на это:

FooAdder<Abc, Def, Ghi>(myThing, 2) ...
Это раздражает, потому что типы могут быть выведены на основе myThing. Поэтому я предпочел бы создать генератор шаблонных объектов, подобный std::make_pair, который будет делать вывод типа для меня. Что-то вроде этого:
template<typename T, typename U, typename V>
FooAdder<T, U, V>
AddFoo(OtherThing<T, U, V> &thing, int a)
{
  return FooAdder<T, U, V>(thing, a);
}

Это кажется проблематичным: поскольку он возвращает значение, стек временный объект будет ( * * ) уничтожен, что приведет к преждевременному запуску RAII dtor.

** - если РВО не реализовано. Большинство компиляторов делают это, но это не требуется, и может быть отключено в gcc с помощью -fno-elide-constructors.

7 4

7 ответов:

Это кажется довольно простым. Спрашивающий сам предложил хорошее решение, но он может просто использовать обычный конструктор копирования с параметром const-reference. Вот что я предложил в комментариях:

template<typename T, typename U, typename V>
class FooAdder
{
private:
  mutable bool dismiss;
  typedef OtherThing<T, U, V> Thing;
  Thing &thing_;
  int a_;
  // many other members
public:
  FooAdder(Thing &thing, int a);
  FooAdder(FooAdder const&o);
  ~FooAdder();
  FooAdder &foo(T t, U u);
  FooAdder &bar(V v);
};

FooAdder::FooAdder(Thing &thing, int a)
  :thing_(thing), a_(a), dismiss(false)
{ }

FooAdder::FooAdder(FooAdder const& o)
  :dismiss(false), thing_(o.thing_), a_(o.a_) 
{ o.dismiss = true; }

FooAdder::~FooAdder() {
  if(!dismiss) { /* wrap up and call */ }
}

Это Просто Работает.

template<typename T, typename U, typename V>
FooAdder<T, U, V>
AddFoo(OtherThing<T, U, V> &thing, int a)
{
  return FooAdder<T, U, V>(thing, a);
}

int main() {
  AddFoo(myThing, 2)
    .foo(3, 4)
    .foo(5, 6)
    .bar(7)
    .foo(8, 9);
}

Нет необходимости в сложных шаблонах или интеллектуальных указателях.

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

Я бы сказал, что здесь, вероятно, очень мало нужно беспокоиться о семантике перемещения (возможно, что она все равно не будет работать - см. auto_ptr_ref хакерство, которое требуется для std::auto_ptr).

Если вы хотите гарантировать, что то, что вы хотите сделать, будет работать без использования семантики перемещения, вам нужно сделать то, что делает auto_ptr, а именно сохранить состояние владения и предоставить оператор преобразования типу, который передает владение между auto_ptrs.

В вашем случае:

  1. добавьте механизм для указания принадлежности в FooAdder. В деструкторе FooAdder's вызывайте функцию очистки только в том случае, если она имеет владельца.
  2. приватизировать конструктор копирования, который принимает const FooAdder &; это предотвращает компилятор от использования конструктора копирования на rvalues, который нарушил бы ваш единственный инвариант владельца.
  3. создайте вспомогательный тип (скажем, FooAdderRef), который будет использоваться для передачи права собственности между FooAdders. Он должен содержать достаточно информации для передачи права собственности.
  4. добавьте оператор преобразования (operator FooAdderRef) в FooAdder, который отказывается от владения в FooAdder и возвращает FooAdderRef.
  5. добавьте конструктор, который берет FooAdderRef и утверждает, что он принадлежит ему.

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

Именно поэтому C++0x имеет семантику перемещения. Потому что иначе это гигантская Пита.

Шаблон друга? (проверено только с gcc)

template <class T, class U, class V> struct OtherThing
{
    void init() { }
    void fini() { }
};

template <class T, class U, class V>
class Adder
{
private:

    typedef OtherThing<T, U, V> Thing;
    Thing& thing_;
    int a_;

    Adder( const Adder& );
    Adder& operator=( const Adder& );

    Adder( Thing& thing, int a ) : thing_( thing ), a_( a ) {}

public:

    ~Adder() { thing_.fini(); }
    Adder& foo( T, U ) { return *this; }
    Adder& bar( V ) { return *this; }

    template <class X, class Y, class Z> friend
        Adder<X,Y,Z> make_adder( OtherThing<X,Y,Z>&, int );
};

template <class T, class U, class V>
Adder<T,U,V> make_adder( OtherThing<T,U,V>& t, int a )
{
    t.init();
    return Adder<T,U,V>( t, a );
}

int main()
{
    OtherThing<int, float, char> ot;
    make_adder( ot, 10 ).foo( 1, 10.f ).bar( 'a'
        ).foo( 10, 1 ).foo( 1, 1 ).bar( '0' );
    return 0;
}

Поскольку C++03 требует явного указания типа в каждом объявлении, это невозможно сделать без динамической типизации, например, когда шаблон наследуется от абстрактного базового класса.

Вы действительно получили что-то умное с

AddFoo(myThing, 2) // OK: it's a factory function
  .foo(3, 4)
  .foo(5, 6)
  .bar(7)
  .foo(8, 9); // but object would still get destroyed here
Но это будет слишком большой болью, чтобы кодировать все в этой цепочке вызовов.

C++0x добавляет auto вычитание типов, поэтому рассмотрите возможность обновления компилятора или его включения, если он у вас есть. (-std=c++0x ВКЛ. ССЗ.)

EDIT: если приведенный выше синтаксис в порядке, но вы хотите иметь несколько цепочек в области видимости, вы можете определить swap с помощью операции void*.

 // no way to have a type-safe container without template specification
 // so use a generic opaque pointer
void *unknown_kinda_foo_handle = NULL;
CreateEmptyFoo(myThing, 2) // OK: it's a factory function
  .foo(3, 4)
  .foo(5, 6)
  .bar(7)
  .foo(8, 9)
  .swap( unknown_kinda_foo_handle ) // keep object, forget its type
  ; // destroy empty object (a la move)

// do some stuff

CreateEmptyFoo(myThing, 2) // recover its type (important! unsafe!)
  .swap( unknown_kinda_foo_handle ) // recover its contents
  .bar( 9 ) // do something
  ; // now it's destroyed properly.
Это ужасно небезопасно, но, кажется, идеально соответствует вашим требованиям.

Править: swap с построенным по умолчанию объектом-это также ответ на эмуляцию move в C++03. Вам нужно добавить конструктор по умолчанию и, возможно, свободное от ресурсов состояние по умолчанию, в котором деструктор ничего не делает.

Вот одно решение, но я подозреваю, что есть и лучшие варианты.

Дайте FooAdder копию ctor с чем-то похожим на семантику перемещения std::auto_ptr. Чтобы сделать это без динамического выделения памяти, копия ctor может установить флаг, указывающий, что dtor не должен выполнять обертку. Вот так:

FooAdder(FooAdder &rhs) // Note: rhs is not const
  : thing_(rhs.thing_)
  , a_(rhs.a_)
  , // etc... lots of other members, annoying.
  , dismiss_(false)
{
  rhs.dismiss_ = true;
}

~FooAdder()
{
  if (!dismiss_)
  {
    // do wrap-up here
  }
}

Вероятно, достаточно отключить оператор присваивания, сделав его частным-не должно быть никакой необходимости вызывать его.

Когда я рассматриваю подобные проблемы, я обычно предпочитаю думать о интерфейсе, который я хочу иметь в первую очередь:

OtherThing<T,U,V> scopedThing = FooAdder(myThing).foo(bla).bar(bla);
Я бы предложил очень простое решение:
template <class T, class U, class V>
class OtherThing: boost::noncopyable
{
public:
  OtherThing(); // if you wish

  class Parameters // may be private if FooAdder is friend
  {
  public:
    template<class,class,class> friend class OtherThing;
    Parameters(int,int,int);
    Parameters(const Parameters& rhs);  // proper resource handling
    ~Parameters();                      // proper resource handling

  private:
    Parameters& operator=(const Parameters&); // disabled

    mutable bool dismiss; // Here is the hack
    int p1;
    int p2;
    int p3;
  }; // Parameters

  OtherThing(const Parameters& p);
};

А затем:

template <class T, class U, class V>
OtherThing<T,U,V>::Parameters fooAdder(Thing<T,U,V> thing, bla_type, bla_type);

Нет необходимости в операторах преобразования и тому подобном, с помощью которых вы рискуете изменить некопируемую семантику, просто создайте временную структуру, из которой будет сконструирован ваш конечный класс, который будет использоваться для передачи всех параметров и изменения семантики этой структуры. настоящий рай. Таким образом, конечный класс OtherThing не имеет завинченной семантики, и гадость (dismiss boolean) надежно спрятана во временном пространстве, которое никогда не должно быть открыто в любом случае.

Вам все еще нужно убедиться в правильной обработке исключений. В частности, это означает, что временный struct отвечает за ресурс, пока он не передан OtherThing.

Я знаю, что это, кажется, не приносит много к столу, так как вы в основном собираетесь взломать Parameters вместо OtherThing, но я призываю вы должны подумать, что бы это значило:

OtherThing<T,U,V> scopedThing = /**/;
OtherThing<T,U,V>* anotherThing = new OtherThing<T,U,V>(scopedThing);

Вторая строка верна для ваших предварительных хаков, так как scopedThing может быть взята как по ссылке, так и по ссылке const, но она портит все так же, как и с std::auto_ptr. В том же духе вы можете иметь std::vector< OtherThing<T,U,V> >, и компилятор никогда не будет жаловаться...