Виртуальный конструктор копий Goo

/ / c++ :: , ,

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

Шаблон может применяться, как для быстрой реализации идиомы виртуального конструктора в C++ для семейств тривиально-конструируемых (и тривиально-копируемых) классов, так и для обуславливания более сложного поведения при копировании экземпляров (например при помощи аллокатора, или с подстановкой дополнительных аргументов в сигнатуру конструктора).

Rationale

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

В качестве примера рассмотрим хранение экземпляров каких-нибудь двух различных классов A и B в классе-коллекции C, помимо хранения экземпляров обеспечивающим доступ к общей функциональности D обоих классов.

Так можно представить отношение классов A, B, C, D на языке UML.

Реализация на C++ будет выглядеть следующим образом (в качестве реализации функциональности, собственно, контейнера C возьмём std::set):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# include <set>

/// Common base for A, B classes, implementing common functionality for C.
class D {
public:
    virtual void foo() = 0;
    // ... bar() = 0, etc.
};  // class D

class A : public D {
public:
    virtual void foo() override {
        std::cout << "I'm an A instance." << std::endl;
    }
    // ... bar() override, etc.
};

class B : public D {
public:
    virtual void foo() override {
        std::cout << "I'm a B instance." << std::endl;
    }
    // ... bar() override, etc.
};

/// Container class, providing common access methods.
class C : public std::set<D *> {
public:
    void for_each_foo() {
        for(auto entry : *this ) {
            entry->foo();
        }
    }
    // ... for_each_bar(), etc.
};

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

Наиболее прямолинейный подход заключается в реализации конструктора копии C::C(const C & orig), в теле которого производится итеративное копирование ассоциированных экземпляров, с определением конкретного типа во время выполнения (посредством dynamic_cast<>(), опирающегося на C++ RTTI):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ... in C
C(const C & orig) {
    for( auto cEntry : orig ) {
        A * a = dynamic_cast<const A *>(cEntry);
        B * b = dynamic_cast<const B *>(cEntry);
        if( a ) {
            insert( new A(*a) );
        } else if( b ) {
            insert( new B(*b) );
        } else {
            throw std::exception();  // unknwon subtype of D
        }
    }
}

Очевидны недостатки такого подхода:

  • Оператор dynamic_cast<>() связан с обращением к RTTI и, будучи размещён в критическом месте программы, может оказать заметное влияние на производительность.
  • Добавление новых подклассов D, которые могут быть помещены в контейнер C требует (с необходимостью) добавления нового if-else-блока в конструтор копии C.

С алгоритмической точки зрения, однако, применение dynamic_cast<>() для подобной операции предстаёт избыточным: полиморфное поведение в C++ традиционно реализуется посредством виртуальных методов, переопределяемых потомками.

Варианты решения

В C++ отсутствует специализированное лексическое средство для определения виртуального конструктора копий (virtual copy ctr), однако его возможно реализовать идиоматически, предусмотрев соответствующий виртуальный метод.

Достаточно популярным способом является установление внутренней конвенции в рамках которой роль такого конструктора будет играть специальный метод. См. например:

Тем не менее, при таком подходе оказывается утраченным удобство пользования обычной лексикой C++. С одной стороны, операции с динамической памятью и аллокаторами должны бы быть явно обозначены в коде, и отказ от обыкновенного конструктора копий выглядит рациональным способом эксплицировать процесс создания копий. С другой стороны, существуют случаи, когда операции с динамической памятью («на куче», "on heap") являются внутренней конвенцией определённой системы классов и их экспликация может быть излишней.

Есть и другие, почти наверняка более громоздкие, идиоматические средства для выражения подобной семантики (см. напр. envelope-letter idiom, рус.).

Специализированный фабричный метод (фабричный конструктор), однако, является на сегодняшний день наиболее признанным паттерном для решения проблемы виртуального конструктора.

Шаблонная реализация виртуального конструктора копий

Отдавая должное удобству использования выделенного виртуального метода для создания копий, нельзя не заметить определённого сходства между тем как обычно реализуется подобный подход:

  • Семейство классов с виртуальным конструтором имеет общий класс-предок.
  • Реализация конструктора виртуального конструктора копий практически всегда включает вызов обыкновенного конструктора копий. Исключения составляют объекты для которых конструктор копий не реализован в обычном смысле.

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

Такой шаблон мог бы параметризоваться:

  • Базовым классом, задающим семейство, от которого будет производиться дальнейшее копирование (класс D в примере).
  • Текущим классом, собственно копирование экземпляра которого должно быть произведено.

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

1
2
3
4
5
6
7
8
9
template< typename BaseT  // family-defined base
        , typename SelfT  // current class
        >
class iDuplicable : public BaseT {
protected:
    virtual BaseT * _V_clone() const override {
        return new SelfT(*this);
    }
};

Использование такого шаблона удобно здесь проиллюстрировать, развивая пример приведённый в начале заметки. В этом случае, использование iDuplicable<> сводится к замене отношения наследования «от D» на «от iDuplicable<T, D>», где T <- A, B. Так, вместо:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class D {
    // ...
};

class A : public D {
    // ...
};

class B : public D {
    // ...
};

следует написать:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class D {
    virtual D * _V_clone() = 0;
    // ...
};

class A : public iDuplicable<D, A> {
    // ...
};

class B : public iDuplicable<D, B> {
    // ...
};

Подобная реализация, однако, создаёт определённую сложность в том случае, если вводится некоторый новый класс E, являющийся потомком A или B за счёт того что реализация _V_clone() вообще говоря, производится в классе-посреднике iDuplicable<>.

Класс E наследует автоматически-созданный виртуальный конструктор A,
копирующий только релевантную A часть.

Эта трудность разрешается добавлением в список параметров шаблона нового параметра-типа:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template< typename BaseT  // family-defined base
        , typename SelfT  // current class
        , typename ParentT  // parent type (has to be descendant of BaseT)
        >
class iDuplicable : public ParentT {
protected:
    virtual BaseT * clone() override {
        return new SelfT(static_cast<const SelfT&>(*this));
    }
};

Заметим, что объявление виртуального метода в чисто-абстрактном базовом классе D можно опустить, если объявить специализацию iDuplicable для случая, когда все три параметра шаблона тождественны:

1
2
3
4
5
6
7
template< typename SelfT >
class iDuplicable<SelfT, SelfT, SelfT> {
protected:
    virtual SelfT * _V_clone() = 0;
public:
    SelfT * clone() { return _V_clone(); }
};

Тогда использование виртуального конструктора копий для набора A, B, D, E выглядит следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class D : public iDuplicable<D, D, D> {
public:
    virtual void foo() = 0;
    // ... bar() = 0, etc.
};

class A : public iDuplicable<D, A, D> {
public:
    virtual void foo() override {
        std::cout << "I'm an A instance." << std::endl;
    }
    // ... bar() override, etc.
};

class B : public iDuplicable<D, B, D> {
public:
    virtual void foo() override {
        std::cout << "I'm a B instance." << std::endl;
    }
    // ... bar() override, etc.
};

class E : public iDuplicable<D, E, B> {
public:
    virtual void foo() override {
        std::cout << "I'm an E instance." << std::endl;
    }
    // ... bar() override, etc.
};

Некоторый (искусственный) пример употребления для иллюстративных целей:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// declaration
A a;
E e;

// pointer type reduction
D * a_d = &a,
  * e_d = &e;

// copying of reduced
D * a_d_copy = a_d->_V_clone(),
  * e_d_copy = e_d->_V_clone();

// checks:
assert( a_d_copy != a_d );
assert( e_d_copy != e_d );
a_d_copy->foo();  // shall identify itself as an A instance
e_d_copy->foo();  // shall identify itself as an E instance

Полный код будет выглядеть следующим образом (публикую сборочный пример для экспериментов):

Подобная реализация оказывается практически эффективной для большинства тривиально-копируемых семейств типов, и полностью закрывает вариант использования для которого вводится виртуальный конструктор «в классическом виде».

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

Расширенная реализация для нетривиально-копируемых классов

Прежде всего, обратим теперь внимание на то что в каждом из набора рассмотренных классов A, B, D, E имелись два сгенерированных компилятором конструктора — тривиальный конструктор (с пустой сигнатурой, напр. A::A()) и конструктор копии «по-умолчанию» (напр. A::A(const A&), далее — тривиальный конструктор копий). Эта ситуация не всегда соответствует практической необходимости, так что обобщённая реализация iDuplicable должна предусматривать случай, когда аргументы конструктора должны быть переданы выше по иерархии наследования.

Для того чтобы охватить случай нетривиально-конструируемых типов, сигнатуру конструктора ParentT в этом случае удобно выразить при помощи шаблонного конструктора iDuplicable с переменным числом параметров. Практически (в iDuplicable):

1
2
template<typename ... CtrArgTs> iDuplicable(CtrArgTs ... ctrargs ) :
        ParentT( ctrargs ... ) {}

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

Такой случай оказывается возможным охватить, привлекая всё тот же приём с переменным числом параметров в конструкторе. Условившись здесь, что сигнатура нетривиального конструктора копий всегда включает в себя константную ссылку на оригинальный экземпляр вначале, запишем шаблон iDuplicable (и его специализацию для базового класса) в виде:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// Common declaration
template< typename BaseT  // family-defined base
        , typename SelfT  // current class
        , typename ParentT  // parent type (has to be descendant of BaseT)
        , typename ... CCtrArgTs
        >
class iDuplicable : public ParentT {
protected:
    virtual BaseT * _V_clone( CCtrArgTs ... ccargs ) override {
        return new SelfT(static_cast<const SelfT &>(*this), ccargs...);
    }
public:
    template<typename ... CtrArgTs> iDuplicable( const ParentT & orig, CtrArgTs ... ctrargs ) :
            ParentT( orig, ctrargs ... ) {}
    template<typename ... CtrArgTs> iDuplicable( CtrArgTs ... ctrargs ) :
            ParentT( ctrargs ... ) {}
};

// Specializtion for abstract bases
template< typename SelfT
        , typename ... CCtrArgTs
        >
class iDuplicable<SelfT, SelfT, SelfT, CCtrArgTs...> {
protected:
    virtual SelfT * _V_clone( CCtrArgTs ... ) = 0;
public:
    SelfT * clone( CCtrArgTs ... ccargs ) { return _V_clone( ccargs ... ); }
};

Для тестовых целей сначала убедимся в совместимости такой модификации с прежним случаем тривиально-конструируемых классов, а затем выберем какой-нибудь единственный дополнительный аргумент в сигнатуре конструктора типа int, и, внеся соответствующие изменения в набор тестовых классов A, B, D, E, убедимся в его работе и в случае расширенных сигнатур.

В расширенном виде, и с возможностью дополнительно специфицировать процедуру копирования в конкретном семействе типов, рассмотренная реализация iDuplicable<> включена в библиотеку Goo.


Comments