Dlaczego C ++ nie pozwala ci wziąć adresu konstruktora?

14

Czy istnieje konkretny powód, dla którego złamałoby to język koncepcyjnie, czy konkretny powód, dla którego jest to technicznie niewykonalne w niektórych przypadkach?

Zastosowanie byłoby z nowym operatorem.

Edycja: Porzucę nadzieję na wyprostowanie mojego „nowego operatora” i „nowego operatora” i będę bezpośredni.

Pytanie brzmi: dlaczego konstruktorzy są wyjątkowi ? Pamiętaj oczywiście, że specyfikacje językowe mówią nam, co jest legalne, ale niekoniecznie moralne. To, co jest legalne, zwykle jest informowane przez to, co logicznie spójne z resztą języka, co jest proste i zwięzłe oraz co jest możliwe do wdrożenia przez kompilatory. Ewentualne uzasadnienie komitetu norm przy ważeniu tych czynników jest celowe i interesujące - stąd pytanie.

Prakseolityczny
źródło
Nie byłoby problemu z podaniem adresu konstruktora, ale z możliwością przekazania typu. Szablony mogą to zrobić.
Euforyczny
Co jeśli masz szablon funkcji, który chcesz skonstruować za pomocą konstruktora, który zostanie określony jako argument funkcji?
Praxeolitic
1
Będą alternatywy dla każdego przykładu, który wymyślę, ale dlaczego konstruktorzy powinni być wyjątkowi? Jest wiele rzeczy, których prawdopodobnie nie użyjesz w większości języków programowania, ale takie przypadki szczególne zwykle mają uzasadnienie.
Praxeolitic
1
@RobertHarvey Pytanie przyszło mi do głowy, kiedy miałem pisać klasę fabryczną.
Praxeolitic
1
Zastanawiam się, czy C ++ 11 std::make_uniquei std::make_sharedmoże odpowiednio rozwiązać praktyczną motywację leżącą u podstaw tego pytania. Są to metody szablonów, co oznacza, że ​​należy przechwycić argumenty wejściowe do konstruktora, a następnie przesłać je do rzeczywistego konstruktora.
rwong

Odpowiedzi:

10

Funkcje wskaźników do członka mają sens tylko wtedy, gdy masz więcej niż jedną funkcję członka z tą samą sygnaturą - w przeciwnym razie wskaźnik byłby tylko jedną możliwą wartością. Nie jest to jednak możliwe w przypadku konstruktorów, ponieważ w C ++ różne konstruktory tej samej klasy muszą mieć różne podpisy.

Alternatywą dla Stroustrup byłoby wybranie składni dla C ++, w której konstruktory mogłyby mieć inną nazwę niż nazwa klasy - ale to zapobiegłoby niektórym bardzo eleganckim aspektom istniejącej składni ctor i uczyniłoby język bardziej skomplikowanym. Dla mnie wygląda to na wysoką cenę, aby pozwolić na rzadko potrzebną funkcję, którą można łatwo zasymulować poprzez „outsourcing” inicjalizacji obiektu z ctor do innej initfunkcji (normalna funkcja składowa, dla której można wskazać wskaźnik do elementów) Utworzony).

Doktor Brown
źródło
2
Ale po co zapobiegać memcpy(buffer, (&std::string)(int, char), size)? (Prawdopodobnie wyjątkowo nie koszerny, ale w końcu jest to C ++.)
Thomas Eding
3
przepraszam, ale to, co napisałeś, nie ma sensu. Nie widzę nic złego w tym, że wskaźnik do elementu wskazuje na konstruktor. wygląda też na to, że coś zacytowałeś, bez linku do źródła.
BЈовић
1
@ThomasEding: czego dokładnie oczekujesz od tego oświadczenia? Kopiujesz gdzieś kod asemblera ctor? Jak zostanie określony „rozmiar” (nawet jeśli spróbujesz czegoś równoważnego dla standardowej funkcji składowej)?
Doc Brown
Spodziewałbym się, że zrobi to samo, co podany adres wskaźnika funkcji swobodnej memcpy(buffer, strlen, size). Prawdopodobnie skopiowałoby to zgromadzenie, ale kto wie. Niezależnie od tego, czy kod mógłby zostać wywołany bez awarii, wymagałaby wiedzy o używanym kompilatorze. To samo dotyczy określania rozmiaru. Byłoby to w dużej mierze zależne od platformy, ale w kodzie produkcyjnym używanych jest wiele nieprzenośnych konstrukcji C ++. Nie widzę powodu, by to zakazać.
Thomas Eding
@ThomasEding: Oczekuje się, że zgodny kompilator C ++ da diagnostykę podczas próby uzyskania dostępu do wskaźnika funkcji, tak jakby był wskaźnikiem danych. Niezgodny kompilator C ++ mógłby zrobić wszystko, ale może również zapewnić sposób dostępu do konstruktora w sposób niezgodny z c ++. To nie jest powód, aby dodać funkcję do C ++, która nie ma zastosowania w zgodnym kodzie.
Bart van Ingen Schenau
5

Konstruktor to funkcja wywoływana, gdy obiekt jeszcze nie istnieje, więc nie może być funkcją składową. Może być statyczny.

Konstruktor faktycznie zostaje wywołany za pomocą tego wskaźnika, po przydzieleniu pamięci, ale przed całkowitą inicjalizacją. W konsekwencji konstruktor ma wiele uprzywilejowanych funkcji.

Gdybyś miał wskaźnik do konstruktora, musiałby to być wskaźnik statyczny, coś w rodzaju funkcji fabrycznej lub specjalny wskaźnik do czegoś, co można nazwać natychmiast po przydzieleniu pamięci. Nie może to być zwykła funkcja składowa i nadal działać jako konstruktor.

Jedynym użytecznym celem, jaki przychodzi mi do głowy, jest specjalny rodzaj wskaźnika, który można przekazać nowemu operatorowi, aby umożliwić mu pośredni wybór konstruktora. Myślę, że to może się przydać, ale wymagałoby to znacznej nowej składni i prawdopodobnie odpowiedź brzmi: pomyśleli o tym i nie było warte wysiłku.

Jeśli chcesz po prostu refaktoryzować wspólny kod inicjujący, zwykła funkcja pamięci jest zwykle wystarczającą odpowiedzią i możesz uzyskać wskaźnik do jednego z nich.

david.pfx
źródło
To wydaje się najbardziej poprawna odpowiedź. Przypominam artykuł sprzed wielu (wielu) lat dotyczący nowego operatora i wewnętrznych działań „nowego operatora”. operator new () przydziela miejsce. Nowy operator wywołuje konstruktor z przydzieloną przestrzenią. Odebranie adresu konstruktora jest „specjalne”, ponieważ wywołanie konstruktora wymaga miejsca. Dostęp do wywołania takiego konstruktora jest możliwy dzięki umieszczeniu new.
Bill Door
1
Słowo „istnieje” przesłania szczegół, że obiekt może mieć adres i przydzielić pamięć, ale nie może zostać zainicjowany. W przypadku funkcji składowej, czy nie, myślę, że pobranie tego wskaźnika powoduje, że funkcja staje się funkcją składową, ponieważ jest wyraźnie powiązana z instancją obiektu (nawet jeśli nie została zainicjowana). To powiedziawszy, odpowiedź podnosi dobrą rację: konstruktor jest jedyną funkcją składową, którą można wywołać na niezainicjowanym obiekcie.
Praxeolitic
Niemniej jednak, najwyraźniej mają one oznaczenie „specjalnych funkcji składowych”. Klauzula 12 standardu C ++ 11: „Domyślny konstruktor (12.1), konstruktor kopiujący i operator przypisania kopii (12.8), konstruktor ruchu i operator przypisania ruchu (12.8) oraz destruktor (12.4) są specjalnymi funkcjami składowymi ”.
Praxeolitic
I 12.1: „Konstruktor nie będzie wirtualny (10.3) ani statyczny (9.4).” (moje podkreślenie)
Praxeolitic
1
Faktem jest, że jeśli kompilujesz z symbolami debugowania i szukasz śladu stosu, to w rzeczywistości jest wskaźnik do konstruktora. Nigdy nie byłem w stanie znaleźć składni, aby uzyskać ten wskaźnik ( &A::Anie działa w żadnym kompilatorze, którego próbowałem).
alfC
-3

Wynika to z faktu, że nie jest to konstruktor typu zwracanego i nie rezerwujesz w pamięci miejsca dla konstruktora. Tak jak robisz w przypadku zmiennej podczas deklaracji. Na przykład: jeśli napiszesz prostą zmienną X, wówczas kompilator wygeneruje błąd, ponieważ kompilator nie zrozumie jego znaczenia. Ale kiedy piszesz Int x; Następnie kompilator dowiaduje się, że jest zmienną typu int, dlatego zarezerwuje miejsce dla zmiennej.

Wniosek: - więc wniosek jest taki, że z powodu wykluczenia typu zwrotu nie zapisze adresu w pamięci.

kocham Goyal
źródło
1
Kod w konstruktorze musi mieć adres w pamięci, ponieważ musi gdzieś być. Nie trzeba rezerwować dla niego miejsca na stosie, ale musi to być gdzieś w pamięci. Państwo może podjąć adres funkcji, które nie zwracają wartości. (void)(*fptr)()deklaruje wskaźnik do funkcji bez wartości zwracanej.
Praxeolitic 10.10.15
2
Pominąłeś punkt pytania - pierwotny post zapytał o pobranie adresu kodu dla konstruktora, a nie wynik podany przez konstruktora. Ponadto, na tej płycie, proszę użyć pełnych słów: „u” nie jest akceptowalnym zamiennikiem dla „ciebie”.
BobDalgleish,
Panie praxeolitic, myślę, że jeśli nie wspomnimy o żadnym typie zwrotu, kompilator nie ustawi konkretnej lokalizacji pamięci dla ctor i ta lokalizacja jest ustawiona wewnętrznie .... Czy możemy pobrać adres dowolnej rzeczy w c ++, która nie jest podana przez kompilator? Jeśli się mylę, popraw mnie poprawną odpowiedzią
uwielbiam Goyal
A także powiedz mi o zmiennej odniesienia. Czy możemy pobrać adres zmiennej odniesienia? Jeśli nie, to jaki adres printf („% u”, & (& (j))); drukuje, jeśli & j = x gdzie x = 10? Ponieważ adres wydrukowany przez printf i adres x nie są takie same
uwielbiam Goyal
-4

Odgadnę dziko:

Konstruktor i destruktor C ++ wcale nie są funkcjami: są makrami. Wchodzą one w zakres, w którym obiekt jest tworzony, i zakres, w którym obiekt jest niszczony. Z kolei nie ma konstruktora ani destruktora, obiekt po prostu JEST.

Właściwie myślę, że inne funkcje w klasie nie są również funkcjami, ale funkcjami wbudowanymi, które NIE są wprowadzane, ponieważ przyjmujesz ich adres (kompilator zdaje sobie sprawę, że na nim jesteś, nie wstawia ani nie wstawia kodu do funkcji i optymalizuje tę funkcję), a z kolei funkcja wydaje się „nadal tam być”, nawet jeśli nie zrobiłbyś tego, gdybyś jej nie wziął pod uwagę.

Wirtualna tabela „obiektu” C ++ nie jest jak obiekt JavaScript, w którym można uzyskać jego konstruktor i tworzyć z niego obiekty w czasie wykonywania new XMLHttpRequest.constructor, ale raczej zbiór wskaźników do anonimowych funkcji, które działają jak środek do komunikacji z tym obiektem , z wyłączeniem możliwości utworzenia obiektu. I nawet nie ma sensu „usuwać” obiektu, ponieważ jest to jak próba usunięcia struktury, nie możesz: to tylko etykieta stosu, po prostu napisz do niego, jak chcesz, pod inną etykietą: możesz użyj klasy jako 4 liczb całkowitych:

/* i imagine this string gets compiled into a struct, one of which's members happens to be a const char * which is initialized to exactly your string: no function calls are made during construction. */
std::string a = "hello, world";
int *myInt = (int *)(*((void **)&a));
myInt[0] = 3;
myInt[1] = 9;
myInt[2] = 20;
myInt[3] = 300;

Nie ma przecieku pamięci, nie ma problemów, oprócz tego, że skutecznie zmarnowałeś garść miejsca na stosie, które jest zarezerwowane dla interfejsu obiektów i łańcucha, ale nie zniszczy twojego programu (dopóki nie spróbujesz go użyć jako ciąg ponownie).

W rzeczywistości, jeśli moje wcześniejsze założenia są poprawne: całkowity koszt łańcucha to tylko koszt przechowywania tych 32 bajtów i stałej przestrzeni łańcucha: funkcje są używane tylko w czasie kompilacji, a po zakończeniu mogą być również wstawiane i odrzucane obiekt jest tworzony i używany (tak jakbyś pracował ze strukturą i odwoływał się do niej bezpośrednio, bez żadnych wywołań funkcji, to na pewno są powielone wywołania zamiast skoków funkcji, ale zwykle jest to szybsze i zajmuje mniej miejsca). W istocie, za każdym razem, gdy wywołujesz dowolną funkcję, kompilator po prostu zastępuje to wywołanie instrukcjami, aby dosłownie to zrobić, z wyjątkami ustawionymi przez projektantów języka.

Podsumowanie: obiekty C ++ nie mają pojęcia, czym są; wszystkie narzędzia do łączenia się z nimi są wbudowane statycznie i utracone w czasie wykonywania. To sprawia, że ​​praca z klasami jest tak wydajna jak wypełnianie struktur danymi i bezpośrednia praca z tymi danymi w ogóle bez wywoływania jakichkolwiek funkcji (funkcje te są wbudowane).

Jest to całkowicie odmienne od podejść COM / ObjectiveC, a także javascript, które dynamicznie przechowują informacje o typie, kosztem narzutu, zarządzania pamięcią, wywołań konstrukcji, ponieważ kompilator nie może wyrzucić tych informacji: jest to konieczne do wysyłki dynamicznej. To z kolei daje nam możliwość „rozmawiania” z naszym programem w czasie wykonywania i rozwijania go podczas działania, dzięki posiadaniu elementów odblaskowych.

Dmitry
źródło
2
przepraszam, ale niektóre części tej „odpowiedzi” są błędne lub niebezpiecznie wprowadzające w błąd. Niestety przestrzeń na komentarze jest zdecydowanie za mała, aby wymienić je wszystkie (większość metod nie zostanie wstawiona, uniemożliwiłoby to wirtualne wysyłanie i nie spowodowało wzdęcia pliku binarnego; nawet jeśli wstawione, może być dostępna adresowalna kopia; nieistotny przykład kodu, który w najgorszy przypadek psuje swój stack aw najlepszym wypadku nie pasuje swoje założenia; ...)
hoffmale
Odpowiedź jest naiwna, chciałem tylko zgadnąć, dlaczego nie można się odwoływać do konstruktora / destruktora. Zgadzam się, że w przypadku klas wirtualnych vtable musi pozostać w pamięci, a adresowalny kod musi być w pamięci, aby vtable mógł się do niego odwoływać. Jednak klasy, które nie implementują klasy wirtualnej, wydają się być wstawiane, jak w przypadku std :: string. Nie wszystko jest wstawiane, ale rzeczy, które nie wydają się być minimalnie umieszczane w „anonimowym” bloku kodu gdzieś w pamięci. W jaki sposób kod psuje stos? Pewnie zgubiliśmy sznurek, ale w przeciwnym razie wszystko, co zrobiliśmy, to reinterpretacja.
Dmitry
Uszkodzenie pamięci występuje w programie komputerowym, gdy zawartość lokalizacji pamięci zostanie przypadkowo zmodyfikowana. Ten program robi to celowo i nie próbuje już używać tego ciągu, więc nie ma korupcji, tylko zmarnowane miejsce na stosie. Ale tak, niezmiennik łańcucha nie jest już utrzymywany, zaśmieca zasięg (na końcu którego odzyskuje się stos).
Dmitry
w zależności od implementacji ciągu możesz zapisać bajty, których nie chcesz. Jeśli ciąg znaków jest podobny struct { int size; const char * data; };(jak się wydaje, że zakładasz), zapisujesz 4 * 4 bajty = 16 bajtów na adres pamięci, w którym zarezerwowałeś tylko 8 bajtów na maszynie x86, więc 8 bajtów jest zapisywanych na innych danych (co może uszkodzić twój stos ). Na szczęście std::stringnormalnie ma pewną optymalizację w miejscu dla krótkich ciągów, więc powinna być wystarczająco duża dla twojego przykładu, gdy używasz dużej implementacji standardowej.
hoffmale
@ hoffmale masz absolutną rację, może to być 4 bajty, może to być 8, a nawet 1 bajt. Jednak gdy poznasz rozmiar łańcucha, wiesz również, że ta pamięć znajduje się na stosie w bieżącym zakresie i możesz z niej korzystać, jak chcesz. Chodzi mi o to, że jeśli znasz strukturę, jest ona zapakowana w sposób niezależny od jakichkolwiek informacji o klasie, w przeciwieństwie do obiektów COM, które mają identyfikator UUID identyfikujący ich klasę jako część vtable IUnknown. Z kolei kompilator uzyskuje dostęp do tych danych bezpośrednio poprzez funkcje wbudowane lub zniekształcone funkcje statyczne.
Dmitry