Żadnej miłości do C ++, jeśli chodzi o linię pytań „ukrytych funkcji”? Pomyślałem, że to tam wyrzucę. Jakie są ukryte funkcje C ++?
Żadnej miłości do C ++, jeśli chodzi o linię pytań „ukrytych funkcji”? Pomyślałem, że to tam wyrzucę. Jakie są ukryte funkcje C ++?
Odpowiedzi:
Większość programistów C ++ zna operator trójskładnikowy:
x = (y < 0) ? 10 : 20;
Jednak nie zdają sobie sprawy, że można go użyć jako lwartości:
(a == 0 ? a : b) = 1;
co jest skrótem dla
if (a == 0)
a = 1;
else
b = 1;
Używaj ostrożnie :-)
(value ? function1 : function2)()
.
function1
i function2
są niejawnie konwertowane na wskaźniki funkcji, a wynik jest niejawnie konwertowany z powrotem.
Możesz umieścić URI w źródle C ++ bez błędów. Na przykład:
void foo() {
http://stackoverflow.com/
int bar = 4;
...
}
goto
którą ma C ++). Cokolwiek po dwóch ukośnikach jest komentarzem. Dlatego z http://stackoverflow.com
, http
jest etykietą (teoretycznie można by to napisać goto http;
) i //stackoverflow.com
jest to tylko komentarz na końcu linii. Oba są legalnym C ++, więc konstrukcja jest kompilowana. Oczywiście nie robi to niczego pożytecznego.
goto http;
rzeczywistości nie podąża za adresem URL. :(
Arytmetyka wskaźników.
Programiści C ++ wolą unikać wskaźników z powodu błędów, które można wprowadzić.
Jednak najfajniejszy C ++, jaki kiedykolwiek widziałem? Literały analogowe.
Zgadzam się z większością tamtejszych postów: C ++ jest językiem wieloparadygmatycznym, więc „ukryte” funkcje, które znajdziesz (inne niż „niezdefiniowane zachowania”, których powinieneś unikać za wszelką cenę) to sprytne wykorzystanie udogodnień.
Większość z tych udogodnień nie jest wbudowanymi funkcjami języka, ale są oparte na bibliotekach.
Najważniejszy jest RAII , często ignorowany przez lata przez programistów C ++ pochodzących ze świata C. Przeciążanie operatorów jest często niezrozumianą funkcją, która umożliwia zarówno zachowanie tablicowe (operator indeksu dolnego), operacje podobne do wskaźnika (inteligentne wskaźniki), jak i operacje podobne do wbudowanych (mnożenie macierzy).
Zastosowanie wyjątku jest często trudne, ale przy odrobinie pracy może stworzyć naprawdę solidny kod dzięki zabezpieczeniu wyjątków specyfikacjom (w tym kod, który nie zawiedzie lub będzie miał funkcje podobne do zatwierdzania, które się powiodą lub powrócą do jego pierwotny stan).
Najbardziej znaną „ukrytą” funkcją C ++ jest metaprogramowanie szablonów , ponieważ pozwala ono na częściowe (lub całkowite) wykonanie programu w czasie kompilacji zamiast w czasie wykonywania. Jest to jednak trudne i zanim spróbujesz, musisz dobrze opanować szablony.
Inne wykorzystują paradygmat wielu do tworzenia „sposobów programowania” spoza przodka C ++, to znaczy C.
Używając funktorów , można symulować funkcje, z dodatkowym zabezpieczeniem typu i stanem. Używając wzorca polecenia , możesz opóźnić wykonanie kodu. Większość innych wzorców projektowych można łatwo i wydajnie zaimplementować w C ++ w celu stworzenia alternatywnych stylów kodowania, których nie powinno się znajdować na liście „oficjalnych paradygmatów C ++”.
Korzystając z szablonów , możesz stworzyć kod, który będzie działał na większości typów, w tym na innym niż ten, o którym myślałeś na początku. Możesz także zwiększyć bezpieczeństwo typów (np. Zautomatyzowany malloc / realloc / free). Funkcje obiektu C ++ są naprawdę potężne (a zatem niebezpieczne, jeśli są używane niedbale), ale nawet dynamiczny polimorfizm ma swoją statyczną wersję w C ++: CRTP .
Odkryłem, że większość książek typu „ Efektywny C ++ ” autorstwa Scotta Meyersa lub „ Wyjątkowy C ++” ” autorstwa Herba Suttera jest zarówno łatwa do czytania, jak i skarbnica informacji o znanych i mniej znanych cechach C ++.
Jednym z moich ulubionych jest taki, który powinien sprawić, że włosy każdego programisty Java wyrosną z horroru: w C ++ najbardziej obiektowym sposobem dodania funkcji do obiektu jest użycie funkcji niebędącej składową nieprzyjazną, zamiast funkcja (czyli metoda klasowa), ponieważ:
W C ++ interfejs klasy to zarówno jej funkcje składowe, jak i funkcje niebędące składowymi w tej samej przestrzeni nazw
nieprzyjazne funkcje niebędące członkami nie mają uprzywilejowanego dostępu do wewnętrznej klasy. W związku z tym użycie funkcji składowej zamiast nieprzyjaznej, niebędącej składnikiem, osłabi hermetyzację klasy.
To zawsze zaskakuje nawet doświadczonych programistów.
(Źródło: między innymi, internetowy Guru tygodnia Herba Suttera nr 84: http://www.gotw.ca/gotw/084.htm )
Jedną z funkcji języka, którą uważam za nieco ukrytą, ponieważ nigdy o niej nie słyszałem przez cały czas w szkole, jest alias przestrzeni nazw. Nie zwróciłem na to mojej uwagi, dopóki nie natknąłem się na przykłady w dokumentacji boost. Oczywiście, skoro już o tym wiem, możesz go znaleźć w każdym standardowym dokumencie C ++.
namespace fs = boost::filesystem;
fs::path myPath( strPath, fs::native );
using
.
W części init for
pętli można deklarować nie tylko zmienne , ale także klasy i funkcje.
for(struct { int a; float b; } loop = { 1, 2 }; ...; ...) {
...
}
To pozwala na wiele zmiennych różnych typów.
Operator tablicy jest asocjacyjny.
A [8] jest synonimem * (A + 8). Ponieważ dodawanie jest asocjacyjne, można to przepisać na * (8 + A), co jest synonimem ..... 8 [A]
Nie powiedziałeś przydatnych ... :-)
A
nie ma żadnego znaczenia. Na przykład, gdyby A
był a char*
, kod nadal byłby ważny.
Niewiele wiadomo, że związki mogą być również szablonami:
template<typename From, typename To>
union union_cast {
From from;
To to;
union_cast(From from)
:from(from) { }
To getTo() const { return to; }
};
Mogą też mieć konstruktory i funkcje składowe. Po prostu nie ma nic wspólnego z dziedziczeniem (w tym funkcjami wirtualnymi).
From
i To
są odpowiednio ustawione i używane. Taka unia może być jednak używana ze zdefiniowanym zachowaniem ( To
jako tablica bez znaku lub struktura dzieląca sekwencję początkową From
). Nawet jeśli używasz go w nieokreślony sposób, może być przydatny do pracy na niskim poziomie. W każdym razie to tylko jeden przykład szablonu unii - mogą istnieć inne zastosowania unii opartej na szablonach.
C ++ to standard, nie powinno być żadnych ukrytych funkcji ...
C ++ jest językiem wieloparadygmatycznym, możesz postawić ostatnie pieniądze na ukryte funkcje. Jeden z wielu przykładów: metaprogramowanie szablonów . Nikt w komitecie normalizacyjnym nie chciał, aby język uzupełniający Turinga był wykonywany w czasie kompilacji.
Inną ukrytą funkcją, która nie działa w C, jest funkcjonalność jednostki jednoargumentowej +
operatora . Możesz go używać do promowania i niszczenia różnych rzeczy
+AnEnumeratorValue
A wartość modułu wyliczającego, która wcześniej miała swój typ wyliczenia, ma teraz doskonały typ liczby całkowitej, który może pasować do jego wartości. Ręcznie prawie nie znasz tego typu! Jest to potrzebne na przykład wtedy, gdy chcesz zaimplementować przeciążony operator do wyliczenia.
Musisz użyć klasy, która używa statycznego inicjatora w klasie bez definicji poza klasą, ale czasami nie można jej połączyć? Operator może pomóc w utworzeniu tymczasowego bez robienia założeń lub zależności od jego typu
struct Foo {
static int const value = 42;
};
// This does something interesting...
template<typename T>
void f(T const&);
int main() {
// fails to link - tries to get the address of "Foo::value"!
f(Foo::value);
// works - pass a temporary value
f(+Foo::value);
}
Czy chcesz przekazać dwa wskaźniki do funkcji, ale to po prostu nie zadziała? Operator może pomóc
// This does something interesting...
template<typename T>
void f(T const& a, T const& b);
int main() {
int a[2];
int b[3];
f(a, b); // won't work! different values for "T"!
f(+a, +b); // works! T is "int*" both time
}
Niewiele osób wie o czasie życia tymczasowych obiektów tymczasowych powiązanych z odniesieniami do stałych. A przynajmniej jest to moja ulubiona część wiedzy C ++, o której większość ludzi nie wie.
const MyClass& x = MyClass(); // temporary exists as long as x is in scope
Przyjemną funkcją, która nie jest często używana, jest blok try-catch obejmujący całą funkcję:
int Function()
try
{
// do something here
return 42;
}
catch(...)
{
return -1;
}
Głównym zastosowaniem byłoby przetłumaczenie wyjątku na inną klasę wyjątków i ponowne zgłoszenie lub tłumaczenie między wyjątkami i obsługą kodu błędu opartego na zwracaniu.
return
z catch bloku Function Try, tylko powtórz.
Wiele osób zna identity
/ id
metafunkcję, ale jest dla niej niezły przypadek użycia dla przypadków innych niż szablony: Łatwość pisania deklaracji:
// void (*f)(); // same
id<void()>::type *f;
// void (*f(void(*p)()))(int); // same
id<void(int)>::type *f(id<void()>::type *p);
// int (*p)[2] = new int[10][2]; // same
id<int[2]>::type *p = new int[10][2];
// void (C::*p)(int) = 0; // same
id<void(int)>::type C::*p = 0;
Bardzo pomaga odszyfrować deklaracje C ++!
// boost::identity is pretty much the same
template<typename T>
struct id { typedef T type; };
template<typename Ret,typename... Args> using function = Ret (Args...); template<typename T> using pointer = *T;
-> pointer<function<void,int>> f(pointer<function<void,void>>);
lub pointer<void(int)> f(pointer<void()>);
lubfunction<pointer<function<void,int>>,pointer<function<void,void>>> f;
Dość ukrytą funkcją jest to, że możesz definiować zmienne w ramach warunku if, a jego zakres będzie obejmował tylko bloki if i else:
if(int * p = getPointer()) {
// do something
}
Niektóre makra tego używają, na przykład w celu zapewnienia pewnego „zablokowanego” zakresu, na przykład:
struct MutexLocker {
MutexLocker(Mutex&);
~MutexLocker();
operator bool() const { return false; }
private:
Mutex &m;
};
#define locked(mutex) if(MutexLocker const& lock = MutexLocker(mutex)) {} else
void someCriticalPath() {
locked(myLocker) { /* ... */ }
}
Również BOOST_FOREACH używa go pod maską. Aby to zrealizować, jest to możliwe nie tylko w if, ale także w przełączniku:
switch(int value = getIt()) {
// ...
}
i za chwilę pętla:
while(SomeThing t = getSomeThing()) {
// ...
}
(a także w stanie). Ale nie jestem pewien, czy te wszystkie są przydatne :)
if((a = f()) == b) ...
, ale ta odpowiedź w rzeczywistości deklaruje zmienną w warunku.
for(...; int i = foo(); ) ...;
To przejdzie przez ciało, o ile i
jest prawdziwe, inicjując je za każdym razem. Pętla, którą pokazujesz, po prostu demonstruje deklarację zmiennej, ale nie deklarację zmiennej, która jednocześnie działa jako warunek :)
Czasami używasz operatora przecinka, ale chcesz się upewnić, że żaden operator przecinka zdefiniowany przez użytkownika nie przeszkadza, ponieważ na przykład polegasz na punktach sekwencji między lewą a prawą stroną lub chcesz się upewnić, że nic nie koliduje z pożądanym akcja. Tutaj void()
do gry wkracza:
for(T i, j; can_continue(i, j); ++i, void(), ++j)
do_code(i, j);
Zignoruj miejsca, które ustawiłem dla warunku i kodu. Ważny jest void()
, co zmusza kompilator do używania wbudowanego operatora przecinka. Może to być przydatne także przy implementowaniu klas cech.
Inicjalizacja tablicy w konstruktorze. Na przykład w klasie, jeśli mamy tablicę int
as:
class clName
{
clName();
int a[10];
};
Możemy zainicjować wszystkie elementy tablicy do wartości domyślnych (tutaj wszystkie elementy tablicy na zero) w konstruktorze jako:
clName::clName() : a()
{
}
Oooh, zamiast tego mogę wymyślić listę nienawiści do zwierząt:
Na plus
Możesz uzyskać dostęp do chronionych danych i elementów składowych funkcji dowolnej klasy, bez niezdefiniowanego zachowania iz oczekiwaną semantyką. Czytaj dalej, aby dowiedzieć się, jak to zrobić. Przeczytaj również raport wady na ten temat.
Zwykle C ++ zabrania dostępu do niestatycznie chronionych elementów składowych obiektu klasy, nawet jeśli ta klasa jest twoją klasą bazową
struct A {
protected:
int a;
};
struct B : A {
// error: can't access protected member
static int get(A &x) { return x.a; }
};
struct C : A { };
To zabronione: Ty i kompilator nie wiecie, na co właściwie wskazuje odniesienie. Może to być C
obiekt, w którym to przypadku klasa B
nie ma żadnego interesu i nie ma pojęcia o swoich danych. Taki dostęp jest udzielany tylko wtedy, gdy x
jest odwołaniem do klasy pochodnej lub klasy pochodnej. I może pozwolić dowolnemu fragmentowi kodu na odczytanie dowolnego chronionego elementu członkowskiego, po prostu tworząc klasę „do wyrzucenia”, która odczytuje elementy, na przykładstd::stack
:
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
static std::deque<int> &get(std::stack<int> &s) {
// error: stack<int>::c is protected
return s.c;
}
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = pillager::get(s);
}
Z pewnością, jak widzisz, spowodowałoby to zbyt duże szkody. Ale teraz wskaźniki dla członków pozwalają na obejście tej ochrony! Kluczową kwestią jest to, że typ wskaźnika elementu członkowskiego jest powiązany z klasą, która faktycznie zawiera wspomniany element członkowski, a nie z klasą określoną podczas pobierania adresu. To pozwala nam ominąć sprawdzanie
struct A {
protected:
int a;
};
struct B : A {
// valid: *can* access protected member
static int get(A &x) { return x.*(&B::a); }
};
struct C : A { };
I oczywiście działa to również na std::stack
przykładzie.
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
static std::deque<int> &get(std::stack<int> &s) {
return s.*(pillager::c);
}
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = pillager::get(s);
}
Będzie to jeszcze łatwiejsze dzięki deklaracji using w klasie pochodnej, która sprawia, że nazwa elementu członkowskiego jest publiczna i odwołuje się do elementu członkowskiego klasy bazowej.
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
using std::stack<int>::c;
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = s.*(&pillager::c);
}
Inną ukrytą funkcją jest możliwość wywoływania obiektów klas, które można konwertować na wskaźniki funkcji lub odwołania. Rozpoznanie przeciążenia jest wykonywane na ich wyniku, a argumenty są doskonale przekazywane.
template<typename Func1, typename Func2>
class callable {
Func1 *m_f1;
Func2 *m_f2;
public:
callable(Func1 *f1, Func2 *f2):m_f1(f1), m_f2(f2) { }
operator Func1*() { return m_f1; }
operator Func2*() { return m_f2; }
};
void foo(int i) { std::cout << "foo: " << i << std::endl; }
void bar(long il) { std::cout << "bar: " << il << std::endl; }
int main() {
callable<void(int), void(long)> c(foo, bar);
c(42); // calls foo
c(42L); // calls bar
}
Są to nazywane „zastępcze funkcje wywoławcze”.
Ukryte funkcje:
Jeśli funkcja zgłasza wyjątek niewymieniony w specyfikacjach wyjątków, ale funkcja ma std::bad_exception
w specyfikacji wyjątku, wyjątek jest konwertowany na std::bad_exception
i generowany automatycznie. W ten sposób będziesz przynajmniej wiedział, że bad_exception
został rzucony. Przeczytaj więcej tutaj .
funkcja try bloków
Słowo kluczowe template w ujednoznacznianiu typów definicji w szablonie klasy. Jeśli nazwa specjalizacji szablonu członkiem pojawia po .
, ->
lub ::
operatora, a nazwa ta ma wyraźnie wykwalifikowanych parametrów szablonu, poprzedź nazwę użytkownika szablonu z szablonu słów kluczowych. Przeczytaj więcej tutaj .
wartości domyślne parametrów funkcji można zmienić w czasie wykonywania. Przeczytaj więcej tutaj .
A[i]
działa tak dobrze, jak i[A]
Tymczasowe instancje klasy można modyfikować! Funkcję składową inną niż stała można wywołać na obiekcie tymczasowym. Na przykład:
struct Bar {
void modify() {}
}
int main (void) {
Bar().modify(); /* non-const function invoked on a temporary. */
}
Przeczytaj więcej tutaj .
Jeśli dwa różne typy występują przed i po wyrażeniu operatora :
ternary ( ?:
), to wynikowy typ wyrażenia jest tym, który jest najbardziej ogólnym z nich. Na przykład:
void foo (int) {}
void foo (double) {}
struct X {
X (double d = 0.0) {}
};
void foo (X) {}
int main(void) {
int i = 1;
foo(i ? 0 : 0.0); // calls foo(double)
X x;
foo(i ? 0.0 : x); // calls foo(X)
}
map::operator[]
tworzy wpis, jeśli brakuje klucza i zwraca odwołanie do domyślnej wartości wpisu. Możesz więc napisać:
map<int, string> m;
string& s = m[42]; // no need for map::find()
if (s.empty()) { // assuming we never store empty values in m
s.assign(...);
}
cout << s;
Jestem zdumiony, ilu programistów C ++ o tym nie wie.
.find()
.
const map::operator[]
generuje komunikaty o błędach”
Umieszczanie funkcji lub zmiennych w bezimiennej przestrzeni nazw unieważnia użycie w static
celu ograniczenia ich do zakresu plików.
static
w zakresie globalnym nie jest w żaden sposób przestarzały. (Dla porównania: C ++ 03 §D.2)
static
użycie powinno być używane tylko w ramach typu klasy lub funkcji.
Definiowanie zwykłych funkcji znajomych w szablonach klas wymaga szczególnej uwagi:
template <typename T>
class Creator {
friend void appear() { // a new function ::appear(), but it doesn't
… // exist until Creator is instantiated
}
};
Creator<void> miracle; // ::appear() is created at this point
Creator<double> oops; // ERROR: ::appear() is created a second time!
W tym przykładzie dwie różne instancje tworzą dwie identyczne definicje - co jest bezpośrednim naruszeniem ODR
Dlatego musimy upewnić się, że parametry szablonu szablonu klasy pojawiają się w typie dowolnej funkcji zaprzyjaźnionej zdefiniowanej w tym szablonie (chyba że chcemy zapobiec więcej niż jednej instancji szablonu klasy w określonym pliku, ale jest to raczej mało prawdopodobne). Zastosujmy to do odmiany naszego poprzedniego przykładu:
template <typename T>
class Creator {
friend void feed(Creator<T>*){ // every T generates a different
… // function ::feed()
}
};
Creator<void> one; // generates ::feed(Creator<void>*)
Creator<double> two; // generates ::feed(Creator<double>*)
Zastrzeżenie: wkleiłem tę sekcję z C ++ Templates: The Complete Guide / Section 8.4
Mało znane, ale poniższy kod jest w porządku
void f() { }
void g() { return f(); }
Jak również ten dziwnie wyglądający
void f() { return (void)"i'm discarded"; }
Wiedząc o tym, możesz skorzystać w niektórych obszarach. Jeden przykład: void
funkcje nie mogą zwracać wartości, ale możesz też nie zwracać niczego, ponieważ mogą być tworzone z wartością non-void. Zamiast przechowywać wartość w zmiennej lokalnej, co spowoduje błąd dla void
, po prostu zwróć wartość bezpośrednio
template<typename T>
struct sample {
// assume f<T> may return void
T dosomething() { return f<T>(); }
// better than T t = f<T>(); /* ... */ return t; !
};
Wczytaj plik do wektora ciągów:
vector<string> V;
copy(istream_iterator<string>(cin), istream_iterator<string>(),
back_inserter(V));
vector<string> V((istream_iterator<string>(cin)), istream_iterator<string>());
- brak nawiasów po drugim parametrze
Możesz tworzyć szablony pól bitowych.
template <size_t X, size_t Y>
struct bitfield
{
char left : X;
char right : Y;
};
Nie wymyśliłem jeszcze żadnego celu, ale na pewno mnie to zaskoczyło.
Jedna z najciekawszych gramatyk ze wszystkich języków programowania.
Trzy z tych rzeczy należą do siebie, a dwie są czymś zupełnie innym ...
SomeType t = u;
SomeType t(u);
SomeType t();
SomeType t;
SomeType t(SomeType(u));
Wszystkie oprócz trzeciego i piątego definiują SomeType
obiekt na stosie i inicjalizują go (z u
w pierwszych dwóch przypadkach i domyślnym konstruktorem w czwartym. Trzeci to deklaracja funkcji, która nie przyjmuje parametrów i zwraca a SomeType
. Piąty jest podobnie deklarowany funkcja, która przyjmuje jeden parametr według wartości typu SomeType
o nazwie u
.
Pozbycie się deklaracji forward:
struct global
{
void main()
{
a = 1;
b();
}
int a;
void b(){}
}
singleton;
Pisanie instrukcji przełączających z?: Operatorami:
string result =
a==0 ? "zero" :
a==1 ? "one" :
a==2 ? "two" :
0;
Robi wszystko w jednej linii:
void a();
int b();
float c = (a(),b(),1.0f);
Zerowanie struktur bez memsetu:
FStruct s = {0};
Normalizowanie / owijanie wartości kąta i czasu:
int angle = (short)((+180+30)*65536/360) * 360/65536; //==-150
Przypisywanie referencji:
struct ref
{
int& r;
ref(int& r):r(r){}
};
int b;
ref a(b);
int c;
*(int**)&a = &c;
FStruct s = {};
jest jeszcze krótsza.
main
? Sugeruję global().main();
i po prostu zapomnieć o Singleton ( można po prostu praca z tymczasowej, która dostaje to żywotność rozszerzony )
Trójskładnikowy operator warunkowy ?:
wymaga , aby jego drugi i trzeci operand miały „zgodne” typy (mówiąc nieformalnie). Ale to wymaganie ma jeden wyjątek (gra słów zamierzona): albo drugi, albo trzeci operand może być wyrażeniem rzucającym (który ma typ void
), niezależnie od typu drugiego operandu.
Innymi słowy, za pomocą ?:
operatora można napisać następujące, poprawnie poprawne wyrażenia C ++
i = a > b ? a : throw something();
Przy okazji, fakt, że wyrażenie throw jest w rzeczywistości wyrażeniem (typu void
), a nie instrukcją, jest kolejną mało znaną cechą języka C ++. Oznacza to między innymi, że poniższy kod jest całkowicie poprawny
void foo()
{
return throw something();
}
chociaż nie ma sensu robić tego w ten sposób (może w jakimś ogólnym kodzie szablonu może się to przydać).
Zasada dominacji jest przydatna, ale mało znana. Mówi się, że nawet jeśli w nieunikalnej ścieżce przez kratkę klasy bazowej, wyszukiwanie nazw dla częściowo ukrytego elementu członkowskiego jest unikalne, jeśli element członkowski należy do wirtualnej klasy bazowej:
struct A { void f() { } };
struct B : virtual A { void f() { cout << "B!"; } };
struct C : virtual A { };
// name-lookup sees B::f and A::f, but B::f dominates over A::f !
struct D : B, C { void g() { f(); } };
Użyłem tego do zaimplementowania obsługi wyrównania, która automatycznie określa najściślejsze dopasowanie za pomocą reguły dominacji.
Dotyczy to nie tylko funkcji wirtualnych, ale także nazw typedef, elementów statycznych / niewirtualnych i czegokolwiek innego. Widziałem, że jest używany do implementowania nadpisywalnych cech w metaprogramach.
struct C
w swoim przykładzie ...? Twoje zdrowie.