Published on
Updated on 

Как эмулировать «супер» ключевое слово в C ++

Authors

Производный класс иногда должен вызывать код своего базового класса и называть его явно.

Но в классы с длинным именем в теле производного класса добавляется много беспорядка. И в C ++ нет ключевых слов super или base для обозначения «базового класса», например, C # и Java.

Одна из причин этого заключается в том, что C ++ поддерживает множественное наследование, что сделает такое ключевое слово неоднозначным. Но, с другой стороны, множественное наследование редко используется. Итак, как производный класс может обозначить свою базу в случае одиночного наследования ?

К счастью, есть способы сделать это, чтобы сделать код производного класса более выразительным.

Повторение имени базового класса?

Существуют две ситуации, когда производный класс должен вызывать имя своей базы.

Вызов конструктора базового класса

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

Но если конструктор принимает некоторые параметры, то мы должны явно называть:

{
public:
    explicit Shape(Color color);
};

class Oval : public Shape
{
public:
    Oval(Color color) : Shape(color)
    {
        ...
    }
};

Вызов реализации базового класса для виртуального метода

{
public:
    virtual void draw() const override
    {
        // base class implementation for draw
    }
};

class Oval : public Shape
{
public:
    void draw() const override
    {
        Shape::draw(); // calls the base class implementation
        ...

Теоретически вам также может потребоваться вызов метода не виртуального базового класса. Но если нужно написать для него имя базового класса, это означает, что у вас есть не виртуальный метод производного класса, который имеет то же имя, что и не виртуальный метод базового класса. И, как объяснено в пункте 36 Эффективного C ++, вы не должны этого делать, потому что производный класс скроет имя метода базового класса, что приведет к неожиданным результатам.

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

Когда это становится проблемой

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

{
public:
    Oval(Color color) : NA::NB::NC::Shape<FirstParameter, SecondParameter, ThirdParameter>(color){}
    
    void draw() const override
    {
        NA::NB::NC::Shape<FirstParameter, SecondParameter, ThirdParameter>::draw();
    }
};

Еа. Это много кода, и он не выражает ничего более, чем предыдущие фрагменты кода. Он просто повторяет одно и то же базовое имя снова и снова.

Здесь мы с завистью смотрим на другие языки. Но еще нет! У C ++ есть то, что нам нужно, чтобы удалить всю эту избыточную информацию.

Вам не нужно писать то, что уже знает компилятор

Одной из целей, которыми руководствуется язык, является освобождение разработчика от работы, которую компилятор может сделать сам по себе. Это хорошо иллюстрируется  auto ключевым словом в C ++ 11 и вычитанием шаблона в конструкторах в C ++ 17.

И даже после C ++ 98 компилятор мог понять, что когда производный класс говорит о « Shape», это означает его базовый класс. Таким образом, вышесказанное в точности эквивалентно:

{
public:
    Oval(Color color) : Shape(color){}
    
    void draw() const override
    {
        Shape::draw();
    }
};

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

Но когда такого выравнивания планет нет, так как используется имя базового класса без параметров шаблона или пространства имен.

Использование typedef

Тем не менее, если вы хотите абстрагировать имя базового класса за выделенным словом, например, super или base, это возможно с помощью typedef или использования объявления.

Один из способов сделать это - добавить объявление using в начале производного класса (private):

class Oval : public NA::NB::NC::Shape<FirstParameter, SecondParameter, ThirdParameter>
{
    using base_type = Shape;
public:
    Oval(Color color) : base_type(color){}
    
    void draw() const override
    {
        base_type::draw();
    }
};

Но есть другое место для использования объявления: в базовом классе. Я впервые увидел это во внедрении библиотеки Boost.

Если вы решите сделать это, имеет смысл разместить объявление в защищенном разделе базового класса. Таким образом, его нельзя смешивать как базовый класс самого базового класса:

template<typename T1, typename T2, typename T3>
class Shape
{
public:
    explicit Shape(Color){}
    virtual void draw() const
    {
        // base class implementation for draw
    }
protected:
    using base_type = Shape;
};

И производные классы могут использовать base_type в своих реализациях без необходимости писать декларацию использования на своем уровне.

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

Конечно, как  super и base ключевые слова других языков, вы можете использовать только объявление using в базовом классе с единственным наследованием, в противном случае существует неопределенность в производном классе.