Termin „interfejs” w C ++

11

Java wyraźnie rozróżnia między classi interface. (Wierzę, że C # też, ale nie mam z tym doświadczenia). Jednak podczas pisania C ++ nie ma wymuszonego rozróżnienia między klasą a interfejsem.

W związku z tym zawsze postrzegałem interfejs jako obejście braku wielokrotnego dziedziczenia w Javie. Dokonanie takiego rozróżnienia jest w C ++ arbitralne i pozbawione znaczenia.

Zawsze stosowałem podejście „pisz rzeczy w najbardziej oczywisty sposób”, więc jeśli w C ++ mam coś, co można nazwać interfejsem w Javie, np .:

class Foo {
public:
  virtual void doStuff() = 0;
  ~Foo() = 0;
};

i wtedy zdecydowałem, że większość implementatorów Foochciała udostępnić jakąś wspólną funkcjonalność, którą prawdopodobnie napisałbym:

class Foo {
public:
  virtual void doStuff() = 0;
  ~Foo() {}
protected:
  // If it needs this to do its thing:
  int internalHelperThing(int);
  // Or if it doesn't need the this pointer:
  static int someOtherHelper(int);
};

Co sprawia, że ​​nie jest to już interfejs w sensie Java.

Zamiast tego C ++ ma dwie ważne koncepcje związane z tym samym podstawowym problemem związanym z dziedziczeniem:

  1. virtual dziedziczenie
  2. Klasy bez zmiennych składowych nie mogą zajmować dodatkowego miejsca, gdy są używane jako baza

    „Podobiekty klasy podstawowej mogą mieć rozmiar zerowy”

    Odniesienie

Spośród tych, których staram się unikać w miarę możliwości numer 1 - rzadko spotyka się scenariusz, w którym naprawdę jest to „najczystszy” projekt. # 2 jest jednak subtelną, ale ważną różnicą między moim rozumieniem terminu „interfejs” a funkcjami języka C ++. W rezultacie obecnie (prawie) nigdy nie nazywam rzeczy „interfejsami” w C ++ i mówię o klasach podstawowych i ich rozmiarach. Powiedziałbym, że w kontekście C ++ „interfejs” jest mylący.

Zwróciłem jednak uwagę, że niewiele osób dokonuje takiego rozróżnienia.

  1. Czy mogę coś stracić, dopuszczając (np. protected) Niefunkcje virtualw „interfejsie” w C ++? (Moje odczucie jest dokładnie odwrotne - bardziej naturalna lokalizacja dla wspólnego kodu)
  2. Czy termin „interfejs” ma znaczenie w C ++ - czy oznacza tylko czysty, virtualczy też sprawiedliwe byłoby nazywanie klas C ++ bez zmiennych składowych nadal interfejsem?
Flexo
źródło
C # ma taki sam wielokrotne dziedziczenie interfejsów, pojedynczego dziedziczenia implementacji jak Java, ale dzięki zastosowaniu ogólnych metod rozszerzenie The internalHelperThingmogą być symulowane w prawie wszystkich przypadkach.
Sjoerd,
Pamiętaj, że ujawnianie wirtualnych członków jest złą praktyką.
Klaim
publiczność ~Foo() {}w klasie abstrakcyjnej jest błędem w (prawie) każdych okolicznościach.
Wolf

Odpowiedzi:

11

W C ++ termin „interfejs” ma nie tylko jedną powszechnie akceptowaną definicję - więc zawsze, gdy będziesz go używać, powinieneś powiedzieć, co masz na myśli - wirtualną klasę bazową z domyślnymi implementacjami lub bez nich, plik nagłówkowy, członkowie publiczni dowolnej klasy i tak dalej.

Jeśli chodzi o twój przykład: w Javie (i podobnym w C #), twój kod prawdopodobnie oznaczałby rozdzielenie problemów:

interface IFoo {/*  ... */} // here is your interface

class FooBase implements IFoo 
{
     // make default implementations for interface methods
}

class Foo extends FooBase
{
}

W C ++ możesz to zrobić, ale nie musisz tego robić. A jeśli chcesz nazwać klasę interfejsem, jeśli nie ma zmiennych składowych, ale zawiera domyślne implementacje niektórych metod, po prostu zrób to, ale upewnij się, że wszyscy, z którymi rozmawiasz, wiedzą, co masz na myśli.

Doktor Brown
źródło
3
Innym możliwym znaczeniem „interfejsu” dla programisty C ++ są publicczęści .hpliku.
David Thornley,
W jakim języku jest twój kod? Jeśli to jest próba bycia C ++, zaraz się rozpłaczę ...
Qix - MONICA BYŁA SZYBKO ZADANA
@Qix: nie przejmuj się, przeczytaj mój post ponownie (wyraźnie stwierdza, że ​​kod jest w Javie), a jeśli masz> 2000 punktów, możesz edytować moje posty, aby dodać równoważny przykładowy kod C ++, aby mój przykład był bardziej jasny.
Doc Brown
Jeśli to jest Java, to nadal jest źle i nie, nie mogę jej edytować; za mało znaków do zmiany. Java nie używa dwukropków do implementacji / rozszerzania, dlatego zastanawiałem się, czy to była próba C ++ ...
Qix - MONICA MASZCZĘDZIAŁO
@Qix: jeśli znajdziesz więcej problemów składniowych, możesz zachować je jako prezent świąteczny :-)
Doc Brown
7

Brzmi trochę tak, jakbyś wpadł w pułapkę mylenia znaczenia tego, że interfejs jest koncepcyjnie zarówno implementacją (interfejs - małe litery „i”), jak i abstrakcją (interfejs - wielkie litery „ja”) ).

Jeśli chodzi o twoje przykłady, twój pierwszy fragment kodu jest tylko klasą. Podczas gdy klasa ma interfejs w tym sensie, że zapewnia metody umożliwiające dostęp do jego zachowania, nie jest to interfejs w sensie deklaracji interfejsu, który zapewnia warstwę abstrakcji reprezentującą rodzaj zachowania, które mogą wymagać od klas wprowadzić w życie. Doc Brown odpowiedź na Twój post pokazuje dokładnie to, co mówię tutaj.

Interfejsy są często reklamowane jako „obejście” dla języków, które nie obsługują wielokrotnego dziedziczenia, ale wydaje mi się, że jest to bardziej nieporozumienie niż twarda prawda (i podejrzewam, że mogę zostać oskarżony o stwierdzenie tego!). Interfejsy naprawdę nie mają nic wspólnego z wielokrotnym dziedziczeniem, ponieważ nie wymagają ani dziedziczenia, ani bycia przodkiem, aby zapewnić funkcjonalną kompatybilność między klasami lub abstrakcję implementacji dla klas. W rzeczywistości, mogą one pozwalają skutecznie pozbyć się dziedziczenia całości powinny chcesz zaimplementować kod w ten sposób - nie, że będę całkowicie go polecam, ale ja tylko twierdzę, że mógłbyZrób to. Tak więc rzeczywistość jest taka, że ​​bez względu na dziedziczenie interfejsy zapewniają środki, za pomocą których można definiować typy klas, ustanawiając reguły określające, w jaki sposób obiekty mogą się ze sobą komunikować, dzięki czemu pozwalają określić, jakie zachowania powinny wspierać klasy narzucając konkretną metodę zastosowaną do wdrożenia tego zachowania.

Czy mogę coś stracić, pozwalając (np. Chronionym) nie-wirtualnym funkcjom na „interfejs” w C ++? (Moje odczucie jest dokładnie odwrotne - bardziej naturalna lokalizacja dla wspólnego kodu)

Czysty interfejs ma być całkowicie abstrakcyjny, ponieważ umożliwia zdefiniowanie umowy zgodności między klasami, które niekoniecznie odziedziczyły swoje zachowanie od wspólnego przodka. Po wdrożeniu chcesz dokonać wyboru, czy zezwolić na rozszerzenie zachowania implementacji w podklasie. Jeśli twoje metody nie są virtual, tracisz możliwość późniejszego przedłużenia tego zachowania, jeśli zdecydujesz, że musisz utworzyć klasy potomne. Niezależnie od tego, czy implementacja jest wirtualna, czy nie, interfejs sam w sobie określa zgodność behawioralną, podczas gdy klasa zapewnia implementację tego zachowania dla instancji reprezentowanych przez klasę.

Czy termin „interfejs” ma znaczenie w C ++ - czy oznacza tylko czysty wirtualny, czy też byłoby sprawiedliwe nazywanie klas C ++ bez zmiennych składowych nadal interfejsem?

Wybacz mi, ale minęło dużo czasu, odkąd napisałem poważną aplikację w C ++. Pamiętam, że Interfejs jest słowem kluczowym do celów abstrakcji, jak je tutaj opisałem. Nie nazwałbym klas C ++ jakimkolwiek interfejsem, powiedziałbym natomiast, że klasa ma interfejs w znaczeniu, które opisałem powyżej. W tym sensie termin JEST znaczący, ale tak naprawdę zależy od kontekstu.

S.Robins
źródło
1
+1 za rozróżnienie między „posiadaniem” a „byciem” interfejsem.
Doc Brown
6

Interfejsy Java nie są „obejściem”, są świadomą decyzją projektową, aby uniknąć niektórych problemów z wielokrotnym dziedziczeniem, takich jak dziedziczenie diamentów, i zachęcić do praktyk projektowych, które minimalizują sprzężenie.

Twój interfejs z metodami chronionymi to podręcznikowy przypadek konieczności „preferowania kompozycji zamiast dziedziczenia”. Cytując Suttera i Alexandrescu w sekcji o tej nazwie z ich doskonałych standardów kodowania C ++ :

Unikaj podatków od spadków: Dziedziczenie jest drugim najściślejszym związkiem łączącym w C ++, ustępującym tylko przyjaźni. Ścisłe połączenie jest niepożądane i należy go unikać, gdy to możliwe. Dlatego wolę kompozycję niż dziedziczenie, chyba że wiesz, że ta ostatnia naprawdę przynosi korzyści Twojemu projektowi.

Uwzględniając funkcje pomocnicze w interfejsie, możesz teraz zaoszczędzić trochę pisania, ale wprowadzasz sprzężenie, które zrani cię na drodze. Prawie zawsze lepiej jest w dłuższej perspektywie rozdzielić funkcje pomocnika i przekazać je Foo*.

STL jest tego dobrym przykładem. Wyciągnięto jak najwięcej funkcji pomocniczych, <algorithm>zamiast być w klasach kontenerów. Na przykład, ponieważ sort()używa publicznego interfejsu API kontenera do wykonania swojej pracy, wiesz, że możesz zaimplementować własny algorytm sortowania bez konieczności zmiany kodu STL. Ten projekt umożliwia bibliotekom takim jak boost, które zwiększają STL zamiast go zastępować, bez potrzeby, aby STL musiał wiedzieć coś o boost.

Karl Bielefeldt
źródło
2

Czy mogę coś stracić, pozwalając (np. Chronionym) nie-wirtualnym funkcjom na „interfejs” w C ++? (Moje odczucie jest dokładnie odwrotne - bardziej naturalna lokalizacja dla wspólnego kodu)

Można by tak pomyśleć, ale umieszczenie chronionej, nie-wirtualnej metody w klasie abstrakcyjnej skłania do implementacji każdego, kto napisze podklasę. Takie postępowanie przeczy celowi interfejsu w jego czystym sensie, jakim jest zapewnienie forniru, który ukrywa to, co pod spodem.

Jest to jeden z tych przypadków, w których nie ma jednej uniwersalnej odpowiedzi, a do podjęcia decyzji musisz wykorzystać swoje doświadczenie i osąd. Jeśli możesz powiedzieć ze 100% pewnością, że każda możliwa podklasa twojej w pełni wirtualnej klasy Foozawsze będzie wymagała implementacji chronionej metody bar(), to Foojest to odpowiednie miejsce. Kiedy masz już podklasę Baz, która nie jest potrzebna bar(), musisz albo żyć z faktem, że Bazbędzie miał dostęp do kodu, którego nie powinien, albo przejść przez proces zmiany hierarchii klas. Ten pierwszy nie jest dobrą praktyką, a drugi może zająć więcej czasu niż kilka minut, które zajęłoby mu właściwe załatwienie sprawy.

Czy termin „interfejs” ma znaczenie w C ++ - czy oznacza tylko czysty wirtualny, czy też byłoby sprawiedliwe nazywanie klas C ++ bez zmiennych składowych nadal interfejsem?

Sekcja 10.4 standardu C ++ wspomina o użyciu klas abstrakcyjnych do implementacji interfejsów, ale nie definiuje ich formalnie. Termin ma znaczenie w ogólnym kontekście informatycznym i każdy kompetentny powinien zrozumieć, że „ Foojest interfejsem dla (czegokolwiek)” implikuje jakąś formę abstrakcji. Osoby z ekspozycją na języki ze zdefiniowanymi konstrukcjami interfejsu mogą myśleć czysto wirtualnie, ale każdy, kto musi faktycznie pracować Foo, przejrzy jego definicję przed kontynuowaniem.

Blrfl
źródło
2
Jaka jest różnica między dostarczeniem czystej wirtualnej deklaracji a implementacją deklaracji plus? W obu przypadkach musi istnieć implementacja i bazzawsze może mieć implementację podobną return false;lub inną. Jeśli metoda nie ma zastosowania do wszystkich podklas, nie należy do żadnej podstawowej klasy abstrakcyjnej.
David Thornley,
1
Myślę, że twoje ostatnie zdanie mówi, że jesteśmy na tej samej stronie: nie ma powodu, dla którego nie można chronić metod w klasach abstrakcyjnych, ale nie powinny one znajdować się wyżej w drzewie dziedziczenia, niż jest to absolutnie konieczne. Po prostu myślę, że jeśli klasa musi zaimplementować implementację, nie jest to odpowiednie miejsce w drzewie.
Blrfl