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

Я хотел бы написать генератор объектов для шаблонного класса 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 заполняют эти структуры данных. ~FooAdder dtor оборачивает вещи и вызывает метод на 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);
}

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

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

Ответы [ 7 ]

3 голосов
/ 30 апреля 2010

Кажется, довольно легко. Сам спрашивающий предложил хорошее решение, но он может просто использовать обычный конструктор копирования с параметром 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);
}

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

1 голос
/ 30 апреля 2010

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

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

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

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

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

1 голос
/ 29 апреля 2010

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

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

0 голосов
/ 30 апреля 2010

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

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

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

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

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

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

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

0 голосов
/ 30 апреля 2010

шаблон друга? (протестировано только с 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;
}
0 голосов
/ 29 апреля 2010

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

Дайте 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
  }
}

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

0 голосов
/ 29 апреля 2010

Поскольку 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 в GCC.)

РЕДАКТИРОВАТЬ: Если приведенный выше синтаксис в порядке, но вы хотите иметь несколько цепочек в области, вы можете определить 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.

Это ужасно небезопасно, но, похоже, идеально соответствует вашим требованиям.

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

...