Использование «супер» в C ++

203

Мой стиль кодирования включает в себя следующую идиому:

class Derived : public Base
{
   public :
      typedef Base super; // note that it could be hidden in
                          // protected/private section, instead

      // Etc.
} ;

Это позволяет мне использовать «super» в качестве псевдонима для Base, например, в конструкторах:

Derived(int i, int j)
   : super(i), J(j)
{
}

Или даже при вызове метода из базового класса внутри его переопределенной версии:

void Derived::foo()
{
   super::foo() ;

   // ... And then, do something else
}

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

class DerivedDerived : public Derived
{
   public :
      typedef Derived super; // note that it could be hidden in
                             // protected/private section, instead

      // Etc.
} ;

void DerivedDerived::bar()
{
   super::bar() ; // will call Derived::bar
   super::super::bar ; // will call Base::bar

   // ... And then, do something else
}

В любом случае, я считаю использование «typedef super» очень полезным, например, когда Base либо многословен и / или шаблонизирован.

Дело в том, что super реализован в Java, а также в C # (где его называют «базовым», если я не ошибаюсь). Но C ++ не хватает этого ключевого слова.

Итак, мои вопросы:

  • такое использование typedef супер распространено / редко / никогда не встречалось в коде, с которым вы работаете?
  • Это использование typedef супер хорошо (то есть вы видите сильные или не очень веские причины не использовать его)?
  • должно ли быть "super" хорошей вещью, должно ли оно быть несколько стандартизировано в C ++, или уже достаточно использования через typedef?

Изменить: Родди упомянул тот факт, что typedef должен быть закрытым. Это будет означать, что любой производный класс не сможет использовать его без повторного выделения. Но я думаю, что это также предотвратит цепочку super :: super (но кто за это будет плакать?).

Редактировать 2: Теперь, спустя несколько месяцев после массового использования «супер», я полностью согласен с точкой зрения Родди: «супер» должен быть частным. Я бы дважды подтвердил его ответ, но, думаю, не смогу.

paercebal
источник
Потрясающие! Именно то, что я искал. Не думаю, что мне когда-либо нужно было использовать эту технику до сих пор. Отличное решение для моего кроссплатформенного кода.
AlanKley
6
По мне так superвыглядит Javaи ничего плохого, но ... Но C++поддерживает множественное наследование.
ST3
2
@ user2623967: Верно. В случае простого наследования достаточно одного «супер». Теперь, если у вас множественное наследование, наличие «superA», «superB» и т. Д. Является хорошим решением: вы хотите вызывать метод из той или иной реализации, поэтому вы должны СКАЗАТЬ, какую реализацию вы хотите. Использование супер-подобного typedef позволяет вам предоставить легко доступное / доступное для записи имя (вместо, скажем, написания MyFirstBase<MyString, MyStruct<MyData, MyValue>>везде)
paercebal
Обратите внимание, что при наследовании от шаблона вам не нужно включать аргументы шаблона при ссылке на него. Например: template <class baz> struct Foo {...void bar() {...} ...}; struct Foo2: Foo<AnnoyinglyLongListOfArguments> { void bar2() { ... Foo::bar(); ...} };это сработало для меня с gcc 9.1 --std = c ++ 1y (c ++ 14).
Танасис Папуцудакис
1
... Гм, коррекция. Кажется, что он работает в любом стандарте с ++, а не только в 14.
Танасис Папуцудакис

Ответы:

151

Бьярн Страуструп упоминает в Design and Evolution of C ++, что superв качестве ключевого слова комитет по стандартам ISO C ++ рассматривал его впервые при стандартизации C ++.

Даг Брук предложил это расширение, назвав базовый класс «унаследованным». В предложении упоминается проблема множественного наследования, и она помечает неоднозначные варианты использования. Даже Страуструп был убежден.

После обсуждения Даг Брук (да, тот же человек, который сделал предложение) написал, что предложение было выполнимо, технически обоснованно, не содержало серьезных недостатков и обрабатывало множественное наследование. С другой стороны, не хватило денег, и комитет должен решить более сложную проблему.

Майкл Тиманн опоздал, а затем показал, что суперопределение типа superdef будет работать просто отлично, используя ту же технику, о которой спрашивалось в этом посте.

Так что нет, это, вероятно, никогда не станет стандартизированным.

Если у вас нет копии, Дизайн и Эволюция вполне стоят стоимости обложки. Использованные копии можно купить за 10 долларов.

Макс Либберт
источник
5
D & E действительно хорошая книга. Но, похоже, мне нужно перечитать его - я не помню ни одной истории.
Майкл Берр
2
Я помню три особенности, которые не были приняты, обсуждались в D & E. Это первое (посмотрите «Майкл Тиманн» в индексе, чтобы найти историю), правило двух недель является вторым (посмотрите «правило двух недель» в индексе), а третье назвали параметрами (посмотрите вверх) именованные аргументы "в индексе".
Макс Либберт
12
В typedefтехнике есть большой недостаток : она не уважает СУХОЙ. Единственный выход - использовать некрасивые макросы для объявления классов. Когда вы наследуете, база может быть длинным многопараметрическим шаблоном класса или хуже. (например, мультиклассы) вам придется переписать все это во второй раз. Наконец, я вижу большую проблему с основами шаблона, которые имеют аргументы класса шаблона. В этом случае супер является шаблоном (а не экземпляром шаблона). Который не может быть определен. Даже в C ++ 11 вам нужно usingдля этого случая.
v.oddou
105

Я всегда использовал «унаследованный», а не супер. (Вероятно, из-за фона Delphi), и я всегда делаю его закрытым , чтобы избежать проблемы, когда «унаследованное» ошибочно исключено из класса, но подкласс пытается его использовать.

class MyClass : public MyBase
{
private:  // Prevents erroneous use by other classes.
  typedef MyBase inherited;
...

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

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

Родди
источник
2
Что касается цепочки super :: super, как я уже упоминал в этом вопросе, мне еще предстоит найти для этого интересное применение. Пока я вижу это только как хак, но это стоило упомянуть, хотя бы из-за различий с Java (где вы не можете связать «супер»).
paercebal
4
Через несколько месяцев я был преобразован в вашу точку зрения (и у меня была ошибка из-за забытого "супер", как вы упомянули ...). Вы совершенно правы в своем ответе, включая цепочку, я думаю. ^ _ ^ ...
paercebal
Как я могу использовать это сейчас для вызова методов родительского класса?
mLstudent33
означает ли это, что я должен объявить все методы Базового класса, virtualкак показано здесь: martinbroadhurst.com/typedef-super.html
mLstudent33
36

Одна из проблем заключается в том, что если вы забудете (пере) определить super для производных классов, то любой вызов super :: что-то будет хорошо скомпилирован, но, вероятно, не вызовет нужную функцию.

Например:

class Base
{
public:  virtual void foo() { ... }
};

class Derived: public Base
{
public:
    typedef Base super;
    virtual void foo()
    {
        super::foo();   // call superclass implementation

        // do other stuff
        ...
    }
};

class DerivedAgain: public Derived
{
public:
    virtual void foo()
    {
        // Call superclass function
        super::foo();    // oops, calls Base::foo() rather than Derived::foo()

        ...
    }
};

(Как отметил Мартин Йорк в комментариях к этому ответу, эту проблему можно устранить, сделав typedef частным, а не общедоступным или защищенным.)

Кристофер Джонсон
источник
Спасибо за замечание. Этот побочный эффект ускользнул от моего внимания. Хотя он, вероятно, не будет компилироваться для использования конструктором, я думаю, что везде будет ошибка.
paercebal
5
Тем не менее, приватный typedef предотвратит цепное использование, упомянутое в оригинальном посте.
AnT
1
Я столкнулся с этим вопросом :(
Стив Вермейлен,
так использовать super1для Base и super2в Derived? DerivedAgain может использовать оба?
mLstudent33
20

FWIW Microsoft добавила расширение для __super в свой компилятор.

krujos
источник
Несколько разработчиков здесь начали использовать __super. Сначала я отодвинулся назад, так как почувствовал, что это «неправильно» и «нестандартно». ОДНАКО, я полюбил это.
Aardvark
8
Я работаю над приложением Windows, и мне нравится расширение __super. Меня огорчает, что комитет по стандартизации отклонил его в пользу упомянутой здесь уловки typedef, потому что, хотя этот трюк typedef хорош, он требует большего обслуживания, чем ключевое слово компилятора, когда вы меняете иерархию наследования, и правильно обрабатывает множественное наследование (без необходимости двух typedefs, такие как super1 и super2). Короче говоря, я согласен с другим комментатором, что расширение MS ОЧЕНЬ полезно, и любой, кто использует Visual Studio исключительно, должен строго рассмотреть возможность его использования.
Брайан
15

Super (или унаследованный) - это очень хорошая вещь, потому что если вам нужно вставить другой слой наследования между Base и Derived, вам нужно всего лишь изменить две вещи: 1. «class Base: foo» и 2. typedef

Если я правильно помню, комитет по стандартам C ++ рассматривал вопрос о добавлении ключевого слова для этого ... пока Майкл Тиманн не указал, что этот трюк с определением типа работает.

Что касается множественного наследования, так как он находится под контролем программиста, вы можете делать все, что захотите: может быть, super1 и super2 или что угодно.

Колин Дженсен
источник
13

Я только что нашел альтернативное решение. У меня есть большая проблема с подходом typedef, который меня укусил сегодня:

  • Для typedef требуется точная копия имени класса. Если кто-то изменит имя класса, но не изменит typedef, у вас возникнут проблемы.

Поэтому я нашел лучшее решение, используя очень простой шаблон.

template <class C>
struct MakeAlias : C
{ 
    typedef C BaseAlias;
};

Так что теперь вместо

class Derived : public Base
{
private:
    typedef Base Super;
};

у тебя есть

class Derived : public MakeAlias<Base>
{
    // Can refer to Base as BaseAlias here
};

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

paperjam
источник
4
publicпсевдоним является недостатком, так как вы открыты для ошибки, упомянутой в ответах Родди и Кристофера (например, вы можете (по ошибке) получить Derivedвместо MakeAlias<Derived>)
Александр Малахов
3
У вас также нет доступа к конструкторам базовых классов в списках инициализаторов. (Это может быть компенсировано в C ++ 11 с использованием наследующих конструкторов MakeAliasили совершенной пересылки, но для этого требуется, чтобы вы ссылались на MakeAlias<BaseAlias>конструкторы, а не просто ссылались на Cконструкторы России напрямую.)
Адам Х. Петерсон
12

Я не помню, чтобы видел это раньше, но на первый взгляд мне это нравится. Как отмечает Ферруччо , это не очень хорошо работает перед лицом MI, но MI - это скорее исключение, чем правило, и нет ничего, что говорило бы о том, что что-то нужно использовать везде, чтобы быть полезным.

Майкл Берр
источник
6
Поднимите только фразу «нет ничего такого, что говорит о том, что что-то должно быть пригодным для использования везде, чтобы быть полезным»
Tanktalus
1
Согласовано. Это полезно. Я просто просматривал библиотеку признаков типа «boost», чтобы увидеть, есть ли способ сгенерировать typedef через шаблон. К сожалению, не похоже, что вы можете.
Ферруччо
9

Я видел эту идиому, используемую во многих кодах, и я почти уверен, что видел ее где-то в библиотеках Boost. Тем не менее, насколько я помню, наиболее распространенное имя base(или Base) вместо super.

Эта идиома особенно полезна при работе с шаблонными классами. В качестве примера рассмотрим следующий класс (из реального проекта ):

template <typename TText, typename TSpec>
class Finder<Index<TText, PizzaChili<TSpec> >, PizzaChiliFinder>
    : public Finder<Index<TText, PizzaChili<TSpec> >, Default>
{
    typedef Finder<Index<TText, PizzaChili<TSpec> >, Default> TBase;
    // …
}

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

Конрад Рудольф
источник
Я использовал «супер» в последнее время по той же причине. Публичное наследство было слишком болезненным, чтобы справиться иначе ... :-) ...
paercebal
4

Я довольно часто видел, как он используется, иногда как super_t, когда база представляет собой сложный тип шаблона ( boost::iterator_adaptorделает это, например)

Джеймс Хопкин
источник
1
Ты прав. Я нашел ссылку здесь. boost.org/doc/libs/1_36_0/libs/iterator/doc/…
paercebal
4

такое использование typedef супер распространено / редко / никогда не встречалось в коде, с которым вы работаете?

Я никогда не видел этот конкретный шаблон в коде C ++, с которым я работаю, но это не значит, что его там нет.

Это использование typedef супер хорошо (то есть вы видите сильные или не очень веские причины не использовать его)?

Это не учитывает множественное наследование (чисто, во всяком случае).

должно ли быть "super" хорошей вещью, должно ли оно быть несколько стандартизировано в C ++, или уже достаточно использования через typedef?

По вышеуказанной причине (множественное наследование) нет. Причина, по которой вы видите «super» в других перечисленных вами языках, заключается в том, что они поддерживают только одиночное наследование, поэтому не возникает путаницы в отношении того, что означает «super». Конечно, в этих языках это полезно, но на самом деле ему нет места в модели данных C ++.

Да, и к вашему сведению: C ++ / CLI поддерживает эту концепцию в форме ключевого слова "__super". Обратите внимание, что C ++ / CLI также не поддерживает множественное наследование.

Тодзи
источник
4
В качестве контрапункта, Perl имеет как множественное наследование, так и SUPER. Существует некоторая путаница, но алгоритм, который проходит виртуальная машина, чтобы найти что-то, четко задокументирован. Тем не менее, я редко вижу, чтобы MI использовался там, где несколько базовых классов предлагают одни и те же методы, где в любом случае может возникнуть путаница.
Tanktalus
1
Microsoft Visual Studio реализует ключевое слово __super для C ++ независимо от того, используется ли оно для CLI. Если только один из унаследованных классов предлагает метод правильной подписи (наиболее распространенный случай, как упомянуто Танкталусом), он просто выбирает единственный допустимый выбор. Если два или более унаследованных класса обеспечивают соответствие функции, это не работает и требует, чтобы вы были явными.
Брайан
3

Еще одна причина использовать typedef для суперкласса - когда вы используете сложные шаблоны в наследовании объекта.

Например:

template <typename T, size_t C, typename U>
class A
{ ... };

template <typename T>
class B : public A<T,99,T>
{ ... };

В классе B было бы идеально иметь typedef для A, иначе вы бы застряли, повторяя его везде, где вы хотели бы сослаться на членов A.

В этих случаях он также может работать с множественным наследованием, но у вас не будет typedef с именем «super», он будет называться «base_A_t» или что-то в этом роде.

--jeffk ++

jdkoftinoff
источник
2

После перехода с Turbo Pascal на C ++ в тот же день я делал это, чтобы получить эквивалент для «унаследованного» ключевого слова Turbo Pascal, которое работает аналогичным образом. Однако после программирования на C ++ на несколько лет я перестал это делать. Я обнаружил, что мне это не очень нужно.

Грег Хьюгилл
источник
1

Я не знаю, редко это или нет, но я, конечно, сделал то же самое.

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

Мэтт Диллард
источник
1

Я использую это время от времени. Просто, когда я обнаружу, что пару раз набираю тип базового класса, я заменю его на typedef, похожий на ваш.

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

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

Скотт Лэнгхэм
источник
1

Я пытался решить ту же самую проблему; Я выдвинул несколько идей, таких как использование шаблонов с переменными числами и расширение пакета для учета произвольного числа родителей, но я понял, что это приведет к реализации, подобной 'super0' и 'super1'. Я уничтожил это, потому что это было бы чуть более полезным, чем отсутствие его для начала.

Мое решение включает вспомогательный класс PrimaryParentи реализовано так:

template<typename BaseClass>
class PrimaryParent : virtual public BaseClass
{
protected:
    using super = BaseClass;
public:
    template<typename ...ArgTypes>
    PrimaryParent<BaseClass>(ArgTypes... args) : BaseClass(args...){}
}

Тогда какой класс вы хотите использовать, будет объявлен так:

class MyObject : public PrimaryParent<SomeBaseClass>
{
public:
    MyObject() : PrimaryParent<SomeBaseClass>(SomeParams) {}
}

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

Причина publicнаследования BaseClassв PrimaryParentсостоит в том, чтобы предоставить MyObjectполный контроль над наследованием, BaseClassнесмотря на наличие класса помощника между ними.

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

Другим ограничением для этого метода является то, что он MyObjectможет наследовать только один класс, от которого он наследуется PrimaryParent, и этот класс должен наследоваться с помощью PrimaryParent. Вот что я имею в виду:

class SomeOtherBase : public PrimaryParent<Ancestor>{}

class MixinClass {}

//Good
class BaseClass : public PrimaryParent<SomeOtherBase>, public MixinClass
{}


//Not Good (now 'super' is ambiguous)
class MyObject : public PrimaryParent<BaseClass>, public SomeOtherBase{}

//Also Not Good ('super' is again ambiguous)
class MyObject : public PrimaryParent<BaseClass>, public PrimaryParent<SomeOtherBase>{}

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

Множественное наследование является сильным инструментом, но в большинстве случаев будет только один первичный родитель, и, если есть другие родители, они, вероятно, будут классами Mixin или классами, которые не наследуются в PrimaryParentлюбом случае. Если множественное наследование все еще необходимо (хотя во многих ситуациях было бы полезно использовать композицию для определения объекта, а не наследования), чем просто явно определить superв этом классе и не наследовать от PrimaryParent.

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

Это может быть только я, хотя.

Конечно, каждая ситуация различна, но учтите, что я сказал, когда решал, какой вариант использовать.

Kevin
источник
1

Я не буду много говорить, кроме настоящего кода с комментариями, который демонстрирует, что супер не означает вызов базы!

super != base.

Короче говоря, что означает "супер" в любом случае? а что значит "база"?

  1. super означает, что вызывается последний разработчик метода (не базовый метод)
  2. база означает, что выбор класса является базовым по умолчанию в множественном наследовании.

Эти 2 правила применяются в классе typedefs.

Рассмотрим разработчика библиотеки и пользователя библиотеки, кто супер, а кто базовый?

Для получения дополнительной информации вот рабочий код для копирования вставьте в вашу IDE:

#include <iostream>

// Library defiens 4 classes in typical library class hierarchy
class Abstract
{
public:
    virtual void f() = 0;
};

class LibraryBase1 :
    virtual public Abstract
{
public:
    void f() override
    {
        std::cout << "Base1" << std::endl;
    }
};

class LibraryBase2 :
    virtual public Abstract
{
public:
    void f() override
    {
        std::cout << "Base2" << std::endl;
    }
};

class LibraryDerivate :
    public LibraryBase1,
    public LibraryBase2
{
    // base is meaningfull only for this class,
    // this class decides who is my base in multiple inheritance
private:
    using base = LibraryBase1;

protected:
    // this is super! base is not super but base!
    using super = LibraryDerivate;

public:
    void f() override
    {
        std::cout << "I'm super not my Base" << std::endl;
        std::cout << "Calling my *default* base: " << std::endl;
        base::f();
    }
};

// Library user
struct UserBase :
    public LibraryDerivate
{
protected:
    // NOTE: If user overrides f() he must update who is super, in one class before base!
    using super = UserBase; // this typedef is needed only so that most derived version
    // is called, which calls next super in hierarchy.
    // it's not needed here, just saying how to chain "super" calls if needed

    // NOTE: User can't call base, base is a concept private to each class, super is not.
private:
    using base = LibraryDerivate; // example of typedefing base.

};

struct UserDerived :
    public UserBase
{
    // NOTE: to typedef who is super here we would need to specify full name
    // when calling super method, but in this sample is it's not needed.

    // Good super is called, example of good super is last implementor of f()
    // example of bad super is calling base (but which base??)
    void f() override
    {
        super::f();
    }
};

int main()
{
    UserDerived derived;
    // derived calls super implementation because that's what
    // "super" is supposed to mean! super != base
    derived.f();

    // Yes it work with polymorphism!
    Abstract* pUser = new LibraryDerivate;
    pUser->f();

    Abstract* pUserBase = new UserBase;
    pUserBase->f();
}

Еще один важный момент здесь это:

  1. полиморфный звонок: звонки вниз
  2. супер звонок: звонки вверх

внутри main()мы используем полиморфные вызовы downards, которые супер звонки вверх, не очень полезные в реальной жизни, но это демонстрирует разницу.

metablaster
источник
0

Это метод, который я использую, который использует макросы вместо typedef. Я знаю, что это не C ++ способ делать вещи, но он может быть удобен, когда цепочки итераторов объединяются посредством наследования, когда только базовый класс, самый дальний по иерархии, действует на унаследованное смещение.

Например:

// some header.h

#define CLASS some_iterator
#define SUPER_CLASS some_const_iterator
#define SUPER static_cast<SUPER_CLASS&>(*this)

template<typename T>
class CLASS : SUPER_CLASS {
   typedef CLASS<T> class_type;

   class_type& operator++();
};

template<typename T>
typename CLASS<T>::class_type CLASS<T>::operator++(
   int)
{
   class_type copy = *this;

   // Macro
   ++SUPER;

   // vs

   // Typedef
   // super::operator++();

   return copy;
}

#undef CLASS
#undef SUPER_CLASS
#undef SUPER

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

Можно использовать строчные буквы, superчтобы воспроизвести поведение, наблюдаемое в Java, но мой стиль кодирования - использовать все прописные буквы для макросов.

Zhro
источник