Використання "супер" в 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 є або багатослівним, і / або шаблонним.

Справа в тому, що супер реалізується в Java, а також в C # (де його називають "базовим", якщо я не помиляюся). Але C ++ не має цього ключового слова.

Отже, мої запитання:

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

Редагувати: Родді згадав той факт, що typedef має бути приватним. Це означає, що будь-який похідний клас не зможе ним користуватися, не переоблаштовуючи його. Але я здогадуюсь, що це також заважатиме супер :: супер ланцюгу (але хто за це плаче?).

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


Дивовижно! Саме те, що я шукав. Не думаю, що мені до цього часу не потрібно було використовувати цю техніку. Відмінне рішення для мого крос-платформного коду.
AlanKley

6
Для мене це superсхоже, Javaі це нічого поганого, але ... Але C++підтримує багаторазове успадкування.
ST3

2
@ user2623967: Правильно. У випадку простого успадкування достатньо одного «супер». Тепер, якщо у вас є багатократне успадкування, наявність "superA", "superB" тощо є хорошим рішенням: ви хочете викликати метод з тієї чи іншої реалізації, тому ви повинні сказати, яку реалізацію ви хочете. Використання типу "super" 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).
Thanasis Papoutsidakis

1
... Гм, виправлення. Здається, він працює у будь-якому стандарті c ++, а не лише у 14.
Thanasis Papoutsidakis

Відповіді:


151

Bjarne Stroustrup в проекті та еволюції C ++ зазначає, що superключовим словом було розглянуто комітетом стандартів ISO C ++ вперше стандартизацію C ++.

Даг Брюк запропонував це розширення, назвавши базовий клас "успадкованим". Пропозиція згадувала питання про багаторазове успадкування та мала б неоднозначне використання. Навіть Строструп був переконаний.

Після обговорення Даг Брюк (так, та сама особа, яка робила пропозицію) написав, що ця пропозиція реалізована, технічно обґрунтована і не має великих недоліків, і вона вирішила багаторазове успадкування. З іншого боку, долара не було достатньо, і комітет повинен вирішити проблему з терніром.

Майкл Тіман прийшов із запізненням, а потім показав, що типовий супер буде добре працювати, використовуючи ту саму техніку, про яку просили у цій посаді.

Отже, ні, це, ймовірно, ніколи не стане стандартизованим.

Якщо у вас немає копії, дизайн та еволюція варто вартістю покриття. Використовувані копії можна було б коштувати приблизно за 10 доларів.


5
D&E - це справді хороша книга. Але здається, що мені потрібно буде її перечитати - я не пам’ятаю жодної історії.
Майкл Берр

2
Я пам'ятаю три особливості, про які не було прийнято обговорювати в науково-дослідній роботі. Це перший (знайдіть "Майкл Тіман" в індексі, щоб знайти історію), правило два тижні - друге (шукайте "правило двох тижнів" в індексі), а третє було названо параметрами (шукайте вгору " названі аргументи "в індексі).
Макс Лібберт

12
У typedefтехніці є головний недолік : вона не поважає ДУХУ. Єдиним способом було б використовувати некрасиві макроси для оголошення класів. Коли ви успадковуєте, базою може бути довгий клас шаблонів з декількома параметрами чи гірше. (наприклад, кілька класів), вам доведеться все це переписати вдруге. Нарешті, я бачу велику проблему з базами шаблонів, які мають аргументи класового шаблону. У цьому випадку супер - це шаблон (а не інстанція шаблону). Який не може бути введений у полезахисту. Навіть на C ++ 11 вам потрібно usingдля цього випадку.
v.oddou

105

Я завжди використовував "успадковані", а не супер. (Можливо, через дельфійський фон), і я завжди роблю це приватним , щоб уникнути проблеми, коли "успадкований" помилково відсутній з класу, але підклас намагається використовувати його.

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

Мій стандартний 'шаблон коду' для створення нових класів включає typedef, тому у мене мало можливості випадково його опустити.

Я не думаю, що прикута пропозиція "супер: супер" є гарною ідеєю. Якщо ви це робите, ви, ймовірно, дуже важко прив'язані до певної ієрархії, і змінивши її, швидше за все, зламаєте речі.


2
Що стосується ланцюжка super :: super, як я вже згадував у запитанні, мені все ж слід знайти цікаву користь для цього. Поки що я розглядаю це лише як хак, але варто було б згадати, якби тільки для розбіжностей з Java (де ви не можете ланцюжок "супер").
paercebal

4
Через кілька місяців мене перевели на вашу точку зору (і у мене DID з'явилася помилка через забутий "супер", як ви згадували ...). Ви абсолютно вірні у своїй відповіді, включаючи прикування, я думаю. ^ _ ^ ...
paercebal

як я зараз використовую це для виклику методів батьківського класу?
mLstudent33

чи означає це, що я повинен оголосити всі методи базового класу, virtualяк показано тут: martinbroadhurst.com/typedef-super.html
mLstudent33

36

Одна з проблем полягає в тому, що якщо ви забудете (повторно) визначити супер для похідних класів, то будь-який виклик до 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 у свій компілятор.


Кілька розробників тут почали наполягати на використанні __super. Спочатку я відсунувся назад, оскільки відчув, що це "неправильно" та "нестандартно". ЯКЩО я вже доросла.
Aardvark

8
Я працюю над додатком Windows і люблю розширення __super. Мене прикро, що комітет стандартів відхилив його на користь згаданого тут трюку typedef, оскільки, хоча цей трюк typedef хороший, він потребує більшого обслуговування, ніж ключове слово компілятора, коли ви змінюєте ієрархію успадкування і правильно обробляє множинне успадкування (не вимагаючи двох typedefs, як super1 і super2). Коротше кажучи, я погоджуюсь з іншим коментатором, що розширення MS дуже корисне, і кожен, хто використовує виключно Visual Studio, повинен настійно розглянути можливість його використання.
Брайан

15

Супер (або успадкований) - це дуже гарна річ, тому що якщо вам потрібно приклеїти ще один спадковий шар між Base та Derived, вам потрібно змінити лише дві речі: 1. "Base class: foo" та 2. typedef

Якщо я пригадую правильно, комітет стандартів C ++ розглядав питання про те, щоб додати ключове слово для цього… поки Майкл Тіман не зазначив, що цей трюк typedef працює.

Що стосується багаторазового успадкування, оскільки це знаходиться під контролем програміста, ви можете робити все, що завгодно: можливо, 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це не приватно, і я намагався захистити від необережного використання, вибравши ім’я типу, яке повинно насторожити інших розробників.


4
publicпсевдонім - це і зворотний бік, оскільки ви відкриті для помилки, згаданої у відповідях Родді та Крістофера (наприклад, ви можете (помилково) вийти Derivedзамість цього MakeAlias<Derived>)
Олександр Малахов,

3
Ви також не маєте доступу до конструкторів базового класу у своїх списках ініціалізаторів. (Це можна компенсувати в C ++ 11, використовуючи успадковувані конструктори MakeAliasабо ідеальну переадресацію, але це вимагає, щоб ви посилалися на MakeAlias<BaseAlias>конструктори, а не просто зверталися Cбезпосередньо до конструкторів.)
Адам Х. Петерсон,

12

Я не пам'ятаю цього раніше, але на перший погляд мені це подобається. Як зазначає Ферруччо , це не дуже добре в умовах ІМ, але ІМ - це швидше виняток, ніж правило, і немає нічого, що говорить, що щось повинно бути корисним скрізь, щоб бути корисним.


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

це використання typedef дуже поширене / рідкісне / ніколи не зустрічалося в коді, з яким ви працюєте?

Я ніколи не бачив цього конкретного шаблону в коді C ++, з яким я працюю, але це не означає, що його там немає.

це використання typedef super Гаразд (тобто ви бачите вагомі чи не такі сильні причини, щоб не використовувати його)?

Це не дозволяє багаторазово успадковувати (чисто, все одно).

має бути "супер" хорошою справою, чи має вона бути дещо стандартизованою в C ++, або цього використання через typedef вже достатньо?

З наведеної вище причини (множинне спадкування) немає. Причина, чому ви бачите "супер" в інших перерахованих вами мовах, полягає в тому, що вони підтримують лише одне успадкування, тому немає ніякої плутанини щодо того, що йдеться про "супер". Зрозуміло, що в цих мовах це корисно, але він насправді не має місця в моделі даних C ++.

Ну, і FYI: C ++ / CLI підтримує цю концепцію у вигляді ключового слова "__super". Зауважте, що C ++ / CLI також не підтримує багаторазове успадкування.


4
Як протилежний момент, Perl має як множинне успадкування, так і SUPER. Існує деяка плутанина, але алгоритм, через який VM проходить, щоб знайти щось, чітко задокументований. Однак, я рідко бачу, що ІМ використовується там, де декілька базових класів пропонують однакові методи, де сплутаність могла виникнути в будь-якому випадку.
Танкталус

1
Microsoft Visual Studio реалізує ключове слово __super для C ++, незалежно від того, чи це для CLI. Якщо лише один із успадкованих класів пропонує метод правильного підпису (найпоширеніший випадок, про який згадував Tanktalus), то він просто вибирає єдиний правильний вибір. Якщо два або більше успадкованих класів забезпечують відповідність функції, воно не працює і вимагає явної ясності.
Брайан

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.

У цих випадках він може працювати і з декількома успадкуваннями, але у вас не було б типу "super", він би називався "base_A_t" або щось подібне.

--jeffk ++


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) {}
}

Щоб уникнути необхідності використовувати віртуальне успадкування в PrimaryParent на 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>{}

Перш ніж відкинути це як варіант через уявну кількість обмежень та факт існування класу середнього чоловіка між кожним спадком, ці речі не є поганими.

Багатократне успадкування - це сильний інструмент, але в більшості випадків буде лише один основний батько, а якщо є інші батьки, то, швидше за все, це будуть класи Міксіна або класи, які не спадкують ні звідки PrimaryParent. Якщо багаторазове успадкування все ще потрібно (хоча в багатьох ситуаціях було б корисно використовувати склад для визначення об'єкта замість успадкування), ніж просто явно визначатиsuper в цьому класі і не успадковувати з цього PrimaryParent.

Ідея того, щоб визначити superв кожному класі не дуже привабливим для мене, використовуючи PrimaryParentдозволяєsuper , явно засновані Спадкування псевдонім, щоб залишитися у визначенні класу лінії замість тіла класу , де дані повинні йти.

Це, можливо, просто я.

Звичайно, кожна ситуація різна, але врахуйте ці речі, які я сказав, вирішуючи, який варіант використовувати.


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()ми використовуємо поліморфні низки дзвінків, які супер дзвонять вгору, не дуже корисні в реальному житті, але це демонструє різницю.



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, але мій стиль кодування полягає у використанні всіх малих літер для великих малі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.