Dlaczego biblioteki i frameworki C ++ nigdy nie używają inteligentnych wskaźników?


156

W kilku artykułach przeczytałem, że surowe wskaźniki prawie nigdy nie powinny być używane. Zamiast tego powinny być zawsze opakowane w inteligentne wskaźniki, niezależnie od tego, czy są to wskaźniki zakresowe, czy wspólne.

Zauważyłem jednak, że frameworki takie jak Qt, wxWidgets i biblioteki takie jak Boost nigdy nie zwracają ani nie oczekują inteligentnych wskaźników, jakby w ogóle ich nie używały. Zamiast tego zwracają lub oczekują surowych wskazówek. Czy jest jakiś powód? Czy powinienem trzymać się z daleka od inteligentnych wskaźników, kiedy piszę publiczny interfejs API, i dlaczego?

Zastanawiam się tylko, dlaczego inteligentne wskaźniki są zalecane, gdy wiele dużych projektów wydaje się ich unikać.


22
Wszystkie wymienione biblioteki powstały wiele lat temu. Inteligentne wskaźniki stały się naprawdę standardem dopiero w C ++ 11.
chrisaycock

22
inteligentne wskaźniki mają narzut (liczenie referencji itp.) - co może być krytyczne - na przykład w systemach wbudowanych / czasu rzeczywistego. IMHO - inteligentne wskaźniki są dla leniwych programistów. Również wiele interfejsów API ma najniższy wspólny mianownik. Kiedy piszę, czuję, jak płomienie liżą moje stopy!
Ed Heal

93
@EdHeal: Powodem, dla którego czujesz, jak płomienie liżą Ci stopy, jest to, że całkowicie się mylisz pod każdym względem. Na przykład, jakie są koszty ogólne unique_ptr? Absolutnie nic. Czy Qt / WxWidgets są przeznaczone dla systemów wbudowanych czy systemów czasu rzeczywistego? Nie, są przeznaczone maksymalnie dla systemów Windows / Mac / Unix na komputerach stacjonarnych. Inteligentne wskazówki są dla programistów, którzy chcą to poprawnie.
Puppy

24
Naprawdę telefony komórkowe obsługują Javę.
R. Martinho Fernandes

12
Inteligentne wskaźniki tylko naprawdę standardowe w C ++ 11? Co??? Te rzeczy były używane od ponad 20 lat.
Kaz,

Odpowiedzi:


124

Pomijając fakt, że wiele bibliotek zostało napisanych przed pojawieniem się standardowych inteligentnych wskaźników, największym powodem jest prawdopodobnie brak standardowego C ++ Application Binary Interface (ABI).

Jeśli piszesz bibliotekę tylko z nagłówkiem, możesz przekazać inteligentne wskaźniki i standardowe pojemniki do treści swojego serca. Ich źródło jest dostępne dla Twojej biblioteki w czasie kompilacji, więc polegasz tylko na stabilności ich interfejsów, a nie ich implementacji.

Jednak ze względu na brak standardowego interfejsu ABI na ogół nie można bezpiecznie przekazywać tych obiektów przez granice modułów. GCC shared_ptrprawdopodobnie różni się od MSVC shared_ptr, który również może różnić się od Intel shared_ptr. Nawet z tym samym kompilatorem nie ma gwarancji, że te klasy będą binarnie zgodne między wersjami.

Najważniejsze jest to, że jeśli chcesz rozprowadzać wstępnie skompilowaną wersję swojej biblioteki, potrzebujesz standardowego ABI, na którym możesz polegać. C go nie ma, ale dostawcy kompilatorów bardzo dobrze oceniają interoperacyjność między bibliotekami C dla danej platformy - istnieją de facto standardy.

Sytuacja nie jest tak dobra dla C ++. Poszczególne kompilatory mogą obsługiwać współdziałanie między swoimi własnymi plikami binarnymi, więc masz możliwość dystrybucji wersji dla każdego obsługiwanego kompilatora, często GCC i MSVC. Ale w świetle tego większość bibliotek po prostu eksportuje interfejs C - a to oznacza surowe wskaźniki.

Kod spoza biblioteki powinien jednak generalnie preferować inteligentne wskaźniki od surowego.


17
Zgadzam się z tobą, nawet przekazanie std :: string może być uciążliwe. To wiele mówi o C ++ jako „świetnym języku dla bibliotek”.
Ha11owed

8
Wniosek jest bardziej podobny: jeśli chcesz rozprowadzać prekompilowaną wersję, musisz to zrobić dla każdego kompilatora, który chcesz wspierać.
josefx

6
@josefx: Tak, to smutne, ale prawda, jedyną alternatywą jest interfejs COM lub surowy interfejs C. Chciałbym, żeby zespół C ++ zaczął się martwić o tego rodzaju problemy. To znaczy, to nie jest tak, że C ++ jest nowym językiem sprzed 2 lat.
Robot Mess

3
Głosowałem w dół, ponieważ to jest złe. W większości przypadków problemy ABI są bardziej niż możliwe do rozwiązania. Chociaż ABI jest mało przyjazny dla użytkownika, jest również trudny do pokonania.
Puppy,

4
@NathanAdams: Takie oprogramowanie jest niewątpliwie imponujące i przydatne. Ale traktuje symptomy głębszych problemów: semantyka czasu życia i własności w C ++ jest gdzieś pomiędzy zubożoną a nieistniejącą. Te sterty błędów nie powstałyby, gdyby język na to nie pozwalał. Tak więc, sprytne wskaźniki nie są panaceum - są one przede wszystkim próbą odzyskania części strat poniesionych przez użycie C ++.
Jon Purdy,

40

Przyczyn może być wiele. Aby wymienić kilka z nich:

  1. Inteligentne wskaźniki niedawno stały się standardem. Do tej pory były częścią innych bibliotek
  2. Ich głównym zastosowaniem jest unikanie wycieków pamięci; wiele bibliotek nie ma własnego zarządzania pamięcią; Zwykle zapewniają narzędzia i interfejsy API
  3. Są implementowane jako opakowanie, ponieważ w rzeczywistości są obiektami, a nie wskaźnikami. Co wiąże się z dodatkowym kosztem czasu / przestrzeni w porównaniu do wskaźników surowych; Użytkownicy bibliotek mogą nie chcieć mieć takich narzutów

Edycja : korzystanie z inteligentnych wskaźników jest całkowicie wyborem programisty. To zależy od różnych czynników.

  1. W systemach o krytycznym znaczeniu dla wydajności możesz nie chcieć używać inteligentnych wskaźników, które generują narzut

  2. Projekt, który wymaga kompatybilności wstecznej, możesz nie chcieć używać inteligentnych wskaźników, które mają specyficzne funkcje C ++ 11

Edit2 Istnieje ciąg kilku głosów przeciw w ciągu 24 godzin z powodu poniższego fragmentu. Nie rozumiem, dlaczego odpowiedź jest odrzucana, mimo że poniżej jest tylko sugestią dodatku, a nie odpowiedzią.
Jednak C ++ zawsze ułatwia otwieranie opcji. :) np

template<typename T>
struct Pointer {
#ifdef <Cpp11>
  typedef std::unique_ptr<T> type;
#else
  typedef T* type;
#endif
};

A w swoim kodzie użyj go jako:

Pointer<int>::type p;

Dla tych, którzy twierdzą, że inteligentny wskaźnik i surowy wskaźnik są różne, zgadzam się z tym. Powyższy kod był tylko pomysłem, w którym można napisać kod, który jest wymienny tylko z a #define, to nie jest przymus ;

Na przykład T*musi zostać wyraźnie usunięty, ale inteligentny wskaźnik nie. Możemy mieć szablon, Destroy()który to obsłuży.

template<typename T>
void Destroy (T* p)
{
  delete p;
}
template<typename T>
void Destroy (std::unique_ptr<T> p)
{
  // do nothing
}

i użyj go jako:

Destroy(p);

W ten sam sposób dla surowego wskaźnika możemy go skopiować bezpośrednio, a dla inteligentnego wskaźnika możemy użyć specjalnej operacji.

Pointer<X>::type p = new X;
Pointer<X>::type p2(Assign(p));

Gdzie Assign()jest jak:

template<typename T>
T* Assign (T *p)
{
  return p;
}
template<typename T>
... Assign (SmartPointer<T> &p)
{
  // use move sematics or whateve appropriate
}

14
Na 3. Niektóre inteligentne wskaźniki mają dodatkowe koszty czasu / przestrzeni, inne nie, w tym std::auto_ptrto , że jest częścią standardu przez długi czas (i zauważ, że lubię std::auto_ptrjako typ zwracany dla funkcji tworzących obiekty, nawet jeśli tak jest prawie bezużyteczne wszędzie indziej). W C ++ 11 std::unique_ptrnie ma dodatkowych kosztów w stosunku do zwykłego wskaźnika.
David Rodríguez - dribeas

4
Dokładnie ... istnieje niezła symetria pojawiania się unique_ptri znikania auto_ptrkodu, który jest przeznaczony dla C ++ 03 powinien używać tego później, podczas gdy kod ukierunkowany na C ++ 11 może używać tego pierwszego. Inteligentne wskazówki nieshared_ptr, istnieje wiele standardów i nie ma ich żadnego, w tym propozycje do standardu, które zostały odrzucone jakomanaged_ptr
David Rodríguez - dribeas Kwietnia

2
@iammilind, to interesujące punkty, ale zabawne jest to, że jeśli w końcu użyjemy inteligentnych wskaźników, jak podobno wielu by polecało, w końcu tworzymy kod niekompatybilny z głównymi bibliotekami. Oczywiście możemy zawijać / rozpakowywać inteligentne wskaźniki w razie potrzeby, ale wydaje się to kłopotliwe i tworzyłby niespójny kod (czasami mamy do czynienia z inteligentnymi wskaźnikami, czasami nie).
laurent

7
Stwierdzenie, że inteligentne wskaźniki mają „dodatkowy koszt czasu / przestrzeni”, jest nieco mylące; wszystkie inteligentne wskaźniki, z wyjątkiem unique_ptrkosztów wykonania, unique_ptrsą zdecydowanie najczęściej używane. Próbka Kod podać również mylące, bo unique_ptri T*są zupełnie różne pojęcia. Fakt, że odnosisz się do nich obu jako, typesprawia wrażenie, że można je zamienić na siebie.
void-pointer

12
Nie możesz tak pisać, te typy nie są w żaden sposób równoważne. Pisanie takich czcionek to proszenie się o kłopoty.
Alex B

35

Istnieją dwa problemy z inteligentnymi wskaźnikami (przed C ++ 11):

  • niestandardowe, więc każda biblioteka ma tendencję do odkrywania na nowo własnych (problemy związane z zespołem i zależnościami NIH)
  • potencjalny koszt

Domyślny inteligentne wskaźnik, tym, że jest wolne od kosztów, jest unique_ptr. Niestety wymaga semantyki przenoszenia w C ++ 11, która pojawiła się dopiero niedawno. Wszystkie inne inteligentne wskaźniki mają koszt ( shared_ptr, intrusive_ptr) lub mają mniej niż idealną semantykę ( auto_ptr).

Z C ++ 11 tuż za rogiem, przynoszącym std::unique_ptr, można by pomyśleć, że to już koniec ... Nie jestem taki optymistyczny.

Tylko kilka głównych kompilatorów implementuje większość C ++ 11 i tylko w ich najnowszych wersjach. Możemy spodziewać się, że duże biblioteki, takie jak QT i Boost, będą przez jakiś czas skłonne zachować zgodność z C ++ 03, co w pewnym stopniu wyklucza szerokie zastosowanie nowych i błyszczących inteligentnych wskaźników.


12

Nie powinieneś trzymać się z daleka od inteligentnych wskaźników, mają one swoje zastosowanie zwłaszcza w aplikacjach, w których musisz przesuwać obiekt.

Biblioteki mają tendencję do zwracania wartości lub wypełniania obiektu. Zwykle nie mają obiektów, których trzeba używać w wielu miejscach, więc nie ma potrzeby, aby używali inteligentnych wskaźników (przynajmniej nie w ich interfejsie, mogą ich używać wewnętrznie).

Mogę na przykład wziąć pod uwagę bibliotekę, nad którą pracowaliśmy, w której po kilku miesiącach rozwoju zdałem sobie sprawę, że używaliśmy wskaźników i inteligentnych wskaźników tylko w kilku klasach (3-5% wszystkich zajęć).

Przekazywanie zmiennych przez odniesienie było wystarczające w większości miejsc, używaliśmy inteligentnych wskaźników, gdy mieliśmy obiekt, który mógł być zerowy, i surowych wskaźników, gdy zmuszała nas do tego biblioteka, której używaliśmy.

Edytuj (nie mogę komentować z powodu mojej reputacji): przekazywanie zmiennych przez odniesienie jest bardzo elastyczne: jeśli chcesz, aby obiekt był tylko do odczytu, możesz użyć odwołania do const (nadal możesz wykonać kilka nieprzyjemnych rzutów, aby móc napisać obiekt ), ale uzyskujesz maksymalną możliwą ochronę (to samo dotyczy inteligentnych wskaźników). Ale zgadzam się, że o wiele przyjemniej jest po prostu zwrócić obiekt.


Nie zgadzam się z tobą, dokładnie, ale zwrócę uwagę, że istnieje szkoła myślenia, która w większości przypadków potępia przekazywanie odwołań do zmiennych . Wyznaję, że należę do tej szkoły. Wolę funkcje, aby nie modyfikowały swoich argumentów. W każdym razie, o ile wiem, odwołania do zmiennych w C ++ nie robią nic, aby zapobiec niewłaściwej obsłudze obiektów, do których się odnoszą, co zamierzają zrobić inteligentne wskaźniki.
thb

2
masz na to const (wydaje mi się, że mogę skomentować: D).
Robot Mess

9

Qt bezcelowo wymyślał na nowo wiele części biblioteki Standard, próbując stać się Javą. Uważam, że obecnie ma swoje własne inteligentne wskazówki, ale ogólnie nie jest to szczyt projektu. wxWidgets, o ile wiem, został zaprojektowany na długo przed napisaniem użytecznych inteligentnych wskaźników.

Jeśli chodzi o Boost, w pełni oczekuję, że będą używać inteligentnych wskaźników wszędzie tam, gdzie jest to właściwe. Być może będziesz musiał być bardziej szczegółowy.

Ponadto nie zapominaj, że istnieją inteligentne wskaźniki wymuszające prawo własności. Jeśli interfejs API nie ma semantyki własności, to po co używać inteligentnego wskaźnika?


19
Qt został napisany, zanim znaczna część funkcji była wystarczająco rozpowszechniona na platformach, z których chciał korzystać. Od dawna ma inteligentne wskaźniki i używa ich do niejawnego współdzielenia zasobów w prawie wszystkich klasach Q *.
rubenvb

6
Każda biblioteka GUI niepotrzebnie wymyśla koło na nowo. Nawet stringi, Qt ma QString, wxWidgets ma wxString, MFC ma strasznie nazwane CString. Czy UTF-8 std::stringnie jest wystarczająco dobry dla 99% zadań GUI?
Inverse

10
@Inverse QString został utworzony, gdy nie było w pobliżu std :: string.
MrFox

Sprawdź, kiedy utworzono qt i jakie inteligentne wskaźniki były wtedy dostępne.
Dainius

3

Dobre pytanie. Nie znam konkretnych artykułów, do których się odnosisz, ale od czasu do czasu czytałem podobne rzeczy. Podejrzewam, że autorzy takich artykułów skłaniają się ku programowaniu w stylu C ++. Jeśli pisarz programuje w C ++ tylko wtedy, gdy musi, a potem wraca do Javy lub tak szybko, jak to możliwe, to tak naprawdę nie podziela sposobu myślenia C ++.

Można podejrzewać, że niektórzy lub większość tych samych autorów preferuje menedżery pamięci zbierające elementy bezużyteczne. Nie, ale myślę inaczej niż oni.

Inteligentne wskaźniki są świetne, ale muszą zachować liczbę referencji. Utrzymanie liczby referencyjnej wiąże się z kosztami - często niewielkimi kosztami, ale mimo to - w czasie wykonywania. Nie ma nic złego w oszczędzaniu tych kosztów przy użyciu samych wskaźników, zwłaszcza jeśli wskaźniki są zarządzane przez destruktory.

Jedną z doskonałych cech C ++ jest wsparcie dla programowania systemów wbudowanych. Częścią tego jest użycie samych wskaźników.

Aktualizacja: komentator poprawnie zauważył, że nowy C ++ unique_ptr(dostępny od TR1) nie liczy referencji. Komentator ma też inną definicję „inteligentnego wskaźnika” niż ja mam na myśli. Może mieć rację co do definicji.

Dalsza aktualizacja: Wątek komentarzy poniżej świeci. Wszystko to jest zalecane do przeczytania.


2
Po pierwsze, programowanie w systemach wbudowanych stanowi ogromną mniejszość całego programowania i jest zupełnie nieistotne. C ++ to język ogólnego przeznaczenia. Po drugie, shared_ptrzachowuje liczbę referencji. Istnieje wiele innych inteligentnych typów wskaźników, które w ogóle nie przechowują liczby odwołań. Wreszcie wspomniane biblioteki są ukierunkowane na platformy, które mają wiele zasobów do stracenia. Nie to, że byłem zwolennikiem krytyki, ale mówię tylko, że Twój post jest pełen błędów.
Puppy

2
@thb - Zgadzam się z tobą. DeadMG - Spróbuj żyć bez systemów wbudowanych. Tak - niektóre inteligentne wskazówki nie mają narzutów, ale niektóre mają. OP wspomina o bibliotekach. Na przykład Boost ma części, które są używane przez systemy wbudowane - ale inteligentne wskaźniki mogą być nieodpowiednie dla niektórych aplikacji.
Ed Heal

2
@EdHeal: Nie żyć bez systemów wbudowanych! = Programowanie dla nich nie jest małą, nieistotną mniejszością. Inteligentne wskaźniki są odpowiednie w każdej sytuacji, w której musisz zarządzać okresem życia zasobu.
Puppy

4
shared_ptrnie ma kosztów ogólnych. Ma narzut tylko wtedy, gdy nie potrzebujesz bezpiecznej wątkowo semantyki współdzielonej własności, która jest tym, co zapewnia.
R. Martinho Fernandes

1
Nie, shared_ptr ma znaczny narzut w stosunku do minimum niezbędnego dla bezpiecznej wątkowo semantyki współdzielonej własności; w szczególności przydziela blok sterty oddzielony od rzeczywistego obiektu, który udostępniasz, wyłącznie w celu przechowywania refcount. intrusive_ptr jest bardziej wydajne, ale (podobnie jak shared_ptr) zakłada również, że każdy wskaźnik do obiektu będzie intrusive_ptr. Możesz uzyskać nawet niższy narzut niż intrusive_ptr dzięki niestandardowemu wskaźnikowi współdzielonemu zliczaniu referencji, tak jak robię to w mojej aplikacji, a następnie użyć T *, gdy tylko możesz zagwarantować, że co najmniej jeden inteligentny wskaźnik przeżyje wartość T *.
Qwertie

2

Istnieją również inne rodzaje inteligentnych wskaźników. Możesz chcieć wyspecjalizowanego inteligentnego wskaźnika do czegoś takiego jak replikacja sieciowa (takiego, który wykrywa, czy jest dostępny i wysyła wszelkie modyfikacje do serwera lub czegoś podobnego), przechowuje historię zmian, zaznacza fakt, że uzyskano do niego dostęp, aby można było go zbadać, kiedy zapisujesz dane na dysku i tak dalej. Nie jestem pewien, czy zrobienie tego we wskaźniku jest najlepszym rozwiązaniem, ale używanie wbudowanych inteligentnych typów wskaźników w bibliotekach może spowodować zamknięcie ludzi w nich i utratę elastyczności.

Ludzie mogą mieć wiele różnych wymagań i rozwiązań w zakresie zarządzania pamięcią poza inteligentnymi wskaźnikami. Może chciałbym sam zarządzać pamięcią, mógłbym przydzielić miejsce na rzeczy w puli pamięci, więc jest przydzielane z góry, a nie w czasie wykonywania (przydatne w grach). Być może używam implementacji C ++ ze zbieraniem śmieci (C ++ 11 umożliwia to, chociaż jeszcze nie istnieje). A może po prostu nie robię nic na tyle zaawansowanego, żeby martwić się, że zawracam im głowę, wiem, że nie zapomnę o niezainicjowanych obiektach i tak dalej. Może po prostu jestem pewien swojej zdolności do zarządzania pamięcią bez kuli wskazującej.

Integracja z C to także inny problem.

Inną kwestią jest to, że inteligentne wskaźniki są częścią STL. C ++ jest przeznaczony do użytku bez STL.


Inną kwestią jest to, że inteligentne wskaźniki są częścią STL. ” Nie są.
curiousguy

0

Zależy to również od domeny, w której pracujesz. Zarabiam na życie pisaniem silników gier, unikamy boostów jak zarazy, w grach narzuty związane z doładowaniem są nie do przyjęcia. W naszym głównym silniku napisaliśmy naszą własną wersję stl (podobnie jak ea stl).

Gdybym miał napisać aplikację do formularzy, mógłbym rozważyć użycie inteligentnych wskaźników; ale kiedy zarządzanie pamięcią jest drugą naturą, brak szczegółowej kontroli nad pamięcią staje się cicho irytujący.


3
Nie ma czegoś takiego jak „koszt doładowania”.
curiousguy

4
Nigdy nie spowolniłem mojego silnika gry w shared_ptr w godnym uwagi stopniu. Przyspieszyli jednak proces produkcji i debugowania. Ponadto, co dokładnie masz na myśli, mówiąc „narzut doładowania”? To całkiem duży koc do rzucenia.
derpface

@curiousguy: To narzut kompilacji wszystkich tych nagłówków i makr + szablon voodoo ...
einpoklum
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.