Саша Ситников (shuffle_c) wrote,
Саша Ситников
shuffle_c

Categories:
  • Music:

Ковариантные возвращаемые типы

Есть в C++ такая фича. Допустим, есть класс с виртуальным методом, возвращающим указатель на себя. Наследуясь от него, можно переопределить этот метод, причем в качестве возвращаемого типа допустимо использование указателя на производный класс. Этот механизм называется ковариантными возвращаемыми типами. Каноническим примером послужит метод clone, создающий копию объекта в куче и возвращающий указатель на него. Примерно так это может выглядеть:
 

struct Clonable
{
    virtual Clonable *clone() const
    {
        return new Clonable(*this);
    }
};

struct B : Clonable
{
    virtual B *clone() const
    {
        return new B(*this);
    }
};

Ковариантные типы полезны тем, что избавляют от излишних приведений типов вниз по иерархии наследования, если используется конкретный тип, а не указатель на его предка. Такое решение доставляет статический контроль типов, что не может не радовать. Однако такая реализация довольно плохая. Во-первых, каждый наследник должен реализовать метод clone, не смотря на то, что код приходится писать один и тот же — с точностью до типа после оператора new. Во-вторых, переопределить метод можно тупо забыть и обнаружить такую ошибку будет непросто. Наконец, в-третьих, в подавляющем большинстве случаев полезно использование умных указателей, в то время как приведенное решение исключает всякую возможность их использования. В общем, все плохо, как обычно, нужно какое-то более гибкое решение.

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

template<typename D>
struct Clonable
{
    virtual D *clone() const
    {
        return new D(*(D *)this);
    }
};

struct B : Clonable<B> { };

struct C : B { };

Теперь наследник сразу имеет правильно работающий метод клонирования. Ой. Что будет с последующими наследниками? А дальше все фейлится. Проблема кроется в том, что наследник C класса B уже не может быть производным от Clonable<C>, содержащим его реализацию клонирования. Точнее говоря, может, например, множественным наследованием, но при этом не произойдет замещения метода, т.к. для него будет создана другая таблица виртуальных функций. Решение этой проблемы предполагает сделать обобщенную реализацию клонирования прокси-наследником.

struct NullType { };

template<typename D, typename Base = NullType>
struct Clonable : public Base
{
    virtual D *clone() const
    {
        return new D(*(D *)this);
    }
};

struct B : Clonable<B> { };

struct C : Clonable<C, B> { };

Да, приходится по другому наследоваться, но это не большая беда. Такой способ был бы уже приемлемым, если бы не возникла одна трудность. Теперь код не компилируется. Резолюция компилятора такова: нельзя переопределять виртуальные методы с разными и нековариантными типами возвращаемого значения. Как же так вышло, что класс C, являясь потомком Clonable<B>, перестал быть ковариантным? Чтобы это понять, достаточно понять, как работает CRTP. Легко видеть, что шаблонный класс Clonable параметризуется, вообще говоря, неполным типом. Однако легальное использование его допустимо, потому что методы шаблонного класса, как и он сам, инстанцируются в точке их вызова, где уже есть полное определение наследника. Но с виртуальными методами отложенное инстанцирование невозможно — компилятор должен вычислить размер таблицы виртуальных функций, которая одинаковая для всех экземпляров класса. Следовательно, при попытке инстанцирования метода clone компилятор немедленно замечает, что класс C не наследник и, значит, не ковариантный.

Итак, как видно, ничего не работает. Поэтому нужно разделить конфликтующие вещи, а для этого взглянем на предложенное решение заново. Какие идиомы оказались полезными? Их всего три: обобщенная реализация клонирования; прокси-наследник; виртуальность метода клонирования. Конфликт, очевидно, с первой и последней идиомами, то есть, фактически, нужен шаблонный виртуальный метод. А таких, как известно, не бывает. Однако есть один важный момент — шаблонным должен быть класс, но не метод. Это обстоятельсво позволяет обойти проблему, воспользовавшись паттерном NVI. Его смысл в предоставлении открытого интерфейса невиртуальными методами, которые реализуются посредствои виртуальных. Такой подход доставляет недостающую нам гибкость.

struct NullType { };

template<typename D, typename Base = NullType>
struct Clonable : public Base
{
    D *clone() const
    {
        return static_cast<D *>(clone_impl());
    }

protected:
    virtual Clonable *clone_impl() const
    {
        return new D(*(D *)this);
    }
};

struct B : Clonable<B> { };

struct C : Clonable<C, B> { };

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

Использованный прием дает еще одно важное приемущество, которое решает последнюю проблему. Теперь возможно использование умных указателей в качестве возвращаемого значения метода clone. В частности, тип указателя может задаваться третьим шаблонным параметром шаблона Clonable.
Правда, остается вопрос. Как не забыть правильно наследоваться? Ну блин, не забывайте.
Subscribe

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 1 comment