Лучший способ написать генератор объектов для класса шаблона 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 ответов:
Это кажется довольно простым. Спрашивающий сам предложил хорошее решение, но он может просто использовать обычный конструктор копирования с параметром 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
.В вашем случае:
- добавьте механизм для указания принадлежности в
FooAdder
. В деструктореFooAdder's
вызывайте функцию очистки только в том случае, если она имеет владельца.- приватизировать конструктор копирования, который принимает
const FooAdder &
; это предотвращает компилятор от использования конструктора копирования на rvalues, который нарушил бы ваш единственный инвариант владельца.- создайте вспомогательный тип (скажем,
FooAdderRef
), который будет использоваться для передачи права собственности междуFooAdders
. Он должен содержать достаточно информации для передачи права собственности.- добавьте оператор преобразования (
operator FooAdderRef
) вFooAdder
, который отказывается от владения вFooAdder
и возвращаетFooAdderRef
.- добавьте конструктор, который берет
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> >
, и компилятор никогда не будет жаловаться...