Dlaczego nie wywnioskować parametru szablonu z konstruktora?

102

Moje dzisiejsze pytanie jest dość proste: dlaczego kompilator nie może wywnioskować parametrów szablonu z konstruktorów klas, podobnie jak może to zrobić z parametrów funkcji? Na przykład, dlaczego następujący kod nie mógł być prawidłowy:

template<typename obj>
class Variable {
      obj data;
      public: Variable(obj d)
              {
                   data = d;
              }
};

int main()
{
    int num = 2;
    Variable var(num); //would be equivalent to Variable<int> var(num),
    return 0;          //but actually a compile error
}

Jak mówię, rozumiem, że to nie jest poprawne, więc moje pytanie brzmi: dlaczego tak nie jest? Czy pozwolenie na to stworzyłoby jakieś poważne luki składniowe? Czy istnieje przypadek, w którym nie chciałoby się tej funkcjonalności (gdzie wnioskowanie typu spowodowałoby problemy)? Po prostu próbuję zrozumieć logikę stojącą za zezwalaniem na wnioskowanie z szablonu dla funkcji, ale nie dla odpowiednio skonstruowanych klas.

GRB
źródło
Zapraszam kogoś (ja to robię, tylko nie teraz), aby skompilował odpowiedź Drahakara i Pitisa (przynajmniej) jako dobre kontrprzykłady, dlaczego to nie może działać
jpinto3912
2
Zwróć również uwagę, że można to łatwo obejść przeztemplate<class T> Variable<T> make_Variable(T&& p) {return Variable<T>(std::forward<T>(p));}
Mooing Duck,
3
Możesz dostać to, czego chcesz var = Variable <decltype (n)> (n);
QuentinUK
18
C ++ 17 na to pozwoli! Ta propozycja została przyjęta: open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
underscore_d
1
@underscore_d Doskonale! Najwyższy czas! Wydawało mi się naturalne, że tak powinno działać i źródłem irytacji, że tak się nie stało.
amdn

Odpowiedzi:

46

Myślę, że nie jest to poprawne, ponieważ konstruktor nie zawsze jest jedynym punktem wejścia klasy (mówię o konstruktorze kopiującym i operatorze =). Załóżmy więc, że używasz swojej klasy w następujący sposób:

MyClass m(string s);
MyClass *pm;
*pm = m;

Nie jestem pewien, czy analizator składni wiedziałby, jaki typ szablonu to MyClass pm;

Nie jestem pewien, czy to, co powiedziałem, ma sens, ale możesz dodać komentarz, to interesujące pytanie.

C ++ 17

Przyjmuje się, że C ++ 17 będzie miał dedukcję typu z argumentów konstruktora.

Przykłady:

std::pair p(2, 4.5);
std::tuple t(4, 3, 2.5);

Akceptowany papier .

Drahakar
źródło
8
To właściwie świetna uwaga, której nigdy nie rozważałem. Nie widzę żadnego sposobu na obejście faktu, że wskaźnik musiałby być specyficzny dla typu (tj. Musiałby to być MyClass <string> * pm). W takim przypadku jedyne, co byś zrobił, to uratowanie się przed określeniem typu podczas tworzenia instancji; kilka zwykłych znaków dodatkowej pracy (i tylko wtedy, gdy obiekt jest wykonany na stosie, a nie na stercie, jak powyżej). Zawsze podejrzewałem, że wnioskowanie klasowe może otworzyć puszkę składni robaków i myślę, że to może być to.
GRB
2
Nie do końca rozumiem, jak zezwolenie na wnioskowanie o parametrach szablonu z konstruktorów wymagałoby zezwolenia na niespecjalizowane deklaracje bez wywołań konstruktora, jak w drugiej linii. Tzn. MyClass *pmTutaj byłoby niepoprawne z tego samego powodu, dla którego zadeklarowana funkcja template <typename T> void foo();nie może zostać wywołana bez wyraźnej specjalizacji.
Kyle Strand
3
@KyleStrand Tak, mówiąc „argumentów szablonu klasy nie można wywnioskować z ich konstruktorów, ponieważ [przykład, w którym nie jest używany żaden konstruktor] ”, ta odpowiedź jest całkowicie nieistotna. Naprawdę nie mogę uwierzyć, że został zaakceptowany, osiągnął +29, zajęło komuś 6 lat, zanim zauważył rażący problem, i siedziałem bez ani jednego przeciwnego głosu przez 7 lat. Czy nikt inny nie myśli, kiedy czytają, czy ...?
underscore_d
1
@underscore_d Podoba mi się, że w obecnym kształcie ta odpowiedź brzmi: „Mogą wystąpić problemy z tą propozycją; Nie jestem pewien, czy to, co właśnie powiedziałem, ma sens (!), nie krępuj się komentować (!!); i och tak przy okazji, tak właśnie wygląda C ++ 17 ”.
Kyle Strand
1
@KyleStrand Ach tak, to kolejny problem, który zauważyłem, ale zapomniałem wymienić wśród wszystkich innych zabaw. Edycja dotycząca C ++ 17 nie została wydana przez OP ... i IMO nie powinna zostać zatwierdzona, ale opublikowana jako nowa odpowiedź: zostałaby odrzucona jako `` zmienia znaczenie postu '', nawet gdyby post miał nie miałam sensu na początku ... Nie zdawałem sobie sprawy, że edycja całkowicie nowych sekcji była uczciwa i na pewno zostały odrzucone mniej drastyczne poprawki, ale myślę, że to szczęście losowania pod względem liczby recenzentów.
underscore_d
27

Nie możesz zrobić tego, o co prosisz, z powodów wskazanych przez inne osoby, ale możesz to zrobić:

template<typename T>
class Variable {
    public: Variable(T d) {}
};
template<typename T>
Variable<T> make_variable(T instance) {
  return Variable<T>(instance);
}

który ze wszystkich zamiarów i celów jest tym samym, o co prosisz. Jeśli kochasz hermetyzację, możesz uczynić make_variable statyczną funkcją składową. Tak ludzie nazywają konstruktora. Więc nie tylko robi to, co chcesz, ale nazywa się to prawie tym, czego chcesz: kompilator wnioskuje parametr szablonu z (nazwanego) konstruktora.

Uwaga: każdy rozsądny kompilator zoptymalizuje tymczasowy obiekt, kiedy napiszesz coś takiego jak

auto v = make_variable(instance);
Lionel
źródło
6
Chcę zauważyć, że nie jest szczególnie przydatne tworzenie statycznego elementu członkowskiego funkcji w takim przypadku, ponieważ w tym celu musiałbyś określić argument szablonu dla klasy, aby mimo to ją wywołać, więc nie ma sensu go dedukować.
Predelnik
3
A jeszcze lepiej w C ++ 11 możesz to zrobić, auto v = make_variable(instance)więc tak naprawdę nie musisz określać typu
Claudiu
1
Tak, lol na myśl o zadeklarowaniu funkcji make jako staticczłonka ... pomyśl o tym przez chwilę. Poza tym: darmowe funkcje czynią były rzeczywiście rozwiązanie, ale jest wiele nadmiarowych boilerplate, że podczas pisania to, po prostu wiem, że nie powinno się ponieważ kompilator ma dostęp do wszystkich informacji jesteś powtarzać. .. i na szczęście C ++ 17 to kanonizuje.
underscore_d
21

W oświeconym wieku 2016 roku, gdy mamy dwa nowe standardy od czasu zadania tego pytania i nowy tuż za rogiem, najważniejsze jest to, aby wiedzieć, że kompilatory obsługujące standard C ++ 17 skompilują kod tak, jak jest .

Odliczenie argumentów-szablonów dla szablonów klas w C ++ 17

Tutaj (dzięki uprzejmości redakcji przyjętej odpowiedzi Olzhasa Zhumabka) znajduje się artykuł szczegółowo opisujący odpowiednie zmiany w standardzie.

Rozwiązywanie problemów wynikających z innych odpowiedzi

Aktualnie najwyżej oceniana odpowiedź

Ta odpowiedź wskazuje, że „konstruktor kopiujący i operator=” nie znałby prawidłowych specjalizacji szablonów.

To nonsens, ponieważ standardowy konstruktor kopiujący operator= istnieje tylko dla znanego typu szablonu:

template <typename T>
class MyClass {
    MyClass(const MyClass&) =default;
    ... etc...
};

// usage example modified from the answer
MyClass m(string("blah blah blah"));
MyClass *pm;   // WHAT IS THIS?
*pm = m;

Oto, jak zauważył w komentarzach, nie ma żadnego powodu, aby MyClass *pmbyć deklaracja prawny z lub bez nowej formy wnioskowania: MyClass Nie jest typem (jest to szablon), więc to nie ma sensu, aby zadeklarować wskaźnik z typ MyClass. Oto jeden możliwy sposób naprawienia tego przykładu:

MyClass m(string("blah blah blah"));
decltype(m) *pm;               // uses type inference!
*pm = m;

Tutaj pmjest już właściwego typu, więc wnioskowanie jest banalne. Co więcej, nie można przypadkowo pomieszać typów podczas wywoływania konstruktora kopiującego:

MyClass m(string("blah blah blah"));
auto pm = &(MyClass(m));

Tutaj pmbędzie wskaźnik do kopii m. Tutaj MyClassjest konstruowana na podstawie kopii - mktóra jest typu MyClass<string>(a nie typu nieistniejącego MyClass). Tak więc, w miejscu, gdzie pm„s typ jest wywnioskować, że jest wystarczająca ilość informacji, aby wiedzieć, że szablon-typ m, a zatem szablon-typ pmjest string.

Co więcej, następujące informacje zawsze powodują błąd kompilacji :

MyClass s(string("blah blah blah"));
MyClass i(3);
i = s;

Dzieje się tak, ponieważ deklaracja konstruktora kopiującego nie jest szablonem:

MyClass(const MyClass&);

W tym przypadku typ szablonu argumentu kopiuj-konstruktora jest zgodny z typem szablonu całej klasy; tj. kiedy MyClass<string>jest tworzony, MyClass<string>::MyClass(const MyClass<string>&);jest tworzony z nim, a kiedy MyClass<int>jest tworzony, MyClass<int>::MyClass(const MyClass<int>&);jest tworzony. O ile nie zostanie to jawnie określone lub nie zostanie zadeklarowany konstruktor oparty na szablonie, nie ma powodu, dla którego kompilator MyClass<int>::MyClass(const MyClass<string>&);miałby tworzyć instancję , co oczywiście byłoby niewłaściwe.

Odpowiedź Cătălin Pitiș

Pitis podaje przykład wyciągania Variable<int>i Variable<double>, po czym stwierdza:

Mam tę samą nazwę typu (zmienna) w kodzie dla dwóch różnych typów (zmienna i zmienna). Z mojego subiektywnego punktu widzenia ma to duży wpływ na czytelność kodu.

Jak zauważono w poprzednim przykładzie, Variablesama w sobie nie jest nazwą typu, mimo że nowa funkcja sprawia, że ​​wygląda jak nazwa.

Pitiș pyta następnie, co by się stało, gdyby nie podano konstruktora, który pozwoliłby na odpowiednie wnioskowanie. Odpowiedź brzmi, że żadne wnioskowanie nie jest dozwolone, ponieważ jest ono wyzwalane przez wywołanie konstruktora . Bez wywołania konstruktora nie ma wnioskowania .

Jest to podobne do pytania o wersję foowydedukowaną tutaj:

template <typename T> foo();
foo();

Odpowiedź brzmi, że ten kod jest nielegalny z podanego powodu.

Odpowiedź MSaltera

O ile mi wiadomo, jest to jedyna odpowiedź, która może wzbudzić uzasadnione obawy dotyczące proponowanej funkcji.

Oto przykład:

Variable var(num);  // If equivalent to Variable<int> var(num),
Variable var2(var); // Variable<int> or Variable<Variable<int>> ?

Kluczowe pytanie brzmi: czy kompilator wybiera tutaj konstruktor wywodzący się z typu , czy konstruktor kopiujący ?

Wypróbowując kod, widzimy, że wybrany został konstruktor kopiujący. Aby rozwinąć przykład :

Variable var(num);          // infering ctor
Variable var2(var);         // copy ctor
Variable var3(move(var));   // move ctor
// Variable var4(Variable(num));     // compiler error

Nie jestem pewien, jak to określa propozycja i nowa wersja normy; wydaje się, że określają go „przewodniki po dedukcji”, które są nowym kawałkiem standardu, którego jeszcze nie rozumiem.

Nie jestem też pewien, dlaczego var4odliczenie jest nielegalne; błąd kompilatora z g ++ wydaje się wskazywać, że instrukcja jest analizowana jako deklaracja funkcji.

Kyle Strand
źródło
Cóż za wspaniała, szczegółowa odpowiedź! var4jest tylko przypadkiem „najbardziej irytującej analizy” (niezwiązanej z dedukcją argumentów szablonu). Kiedyś używaliśmy do tego po prostu dodatkowych parenów, ale obecnie myślę, że używanie nawiasów klamrowych do jednoznacznego oznaczenia konstrukcji jest zwykłą radą.
Sumudu Fernando
@SumuduFernando Thanks! Czy masz na myśli, że Variable var4(Variable(num));jest to traktowane jako deklaracja funkcji? Jeśli tak, dlaczego jest Variable(num)to prawidłowa specyfikacja parametru?
Kyle Strand
@SumuduFernando Nieważne, nie miałem pojęcia, że ​​to ważne: coliru.stacked-crooked.com/a/98c36b8082660941
Kyle Strand
11

Wciąż brakuje: sprawia, że ​​następujący kod jest dość niejednoznaczny:

int main()
{
    int num = 2;
    Variable var(num);  // If equivalent to Variable<int> var(num),
    Variable var2(var); //Variable<int> or Variable<Variable<int>> ?
}
MSalters
źródło
Kolejna dobra uwaga. Zakładając, że istnieje zmienna zdefiniowana przez konstruktor kopiujący (Variable <obj> d), musiałby być ustalony jakiś rodzaj pierwszeństwa.
GRB
1
Lub, alternatywnie, niech kompilator ponownie wyrzuci błąd niezdefiniowanego parametru szablonu, podobnie jak zasugerowałem w odniesieniu do odpowiedzi Pitis. Jeśli jednak wybierzesz tę drogę, liczba przypadków, w których wnioskowanie może się odbyć bez problemów (błędów), staje się coraz mniejsza.
GRB
Właściwie jest to interesująca kwestia i (jak zauważyłem w mojej odpowiedzi) nie jestem jeszcze pewien, jak zaakceptowana propozycja C ++ 17 rozwiązuje ten problem.
Kyle Strand
9

Przypuśćmy, że kompilator obsługuje to, o co prosiłeś. Wtedy ten kod jest ważny:

Variable v1( 10); // Variable<int>

// Some code here

Variable v2( 20.4); // Variable<double>

Teraz mam tę samą nazwę typu (zmienna) w kodzie dla dwóch różnych typów (zmienna i zmienna). Z mojego subiektywnego punktu widzenia ma to duży wpływ na czytelność kodu. Posiadanie tej samej nazwy typu dla dwóch różnych typów w tej samej przestrzeni nazw wydaje mi się mylące.

Późniejsza aktualizacja: Kolejna rzecz do rozważenia: częściowa (lub pełna) specjalizacja szablonów.

A co, jeśli specjalizuję się w zmiennej i nie dostarczam konstruktora takiego, jakiego oczekujesz?

Więc miałbym:

template<>
class Variable<int>
{
// Provide default constructor only.
};

Następnie mam kod:

Variable v( 10);

Co powinien zrobić kompilator? Użyj ogólnej definicji klasy Variable, aby wywnioskować, że jest to zmienna, a następnie przekonaj się, że zmienna nie zapewnia jednego konstruktora parametrów?

Cătălin Pitiș
źródło
1
Gorzej: co jeśli masz tylko Variable <int> :: Variable (float)? Masz teraz dwa sposoby na wydedukowanie zmiennej (1f) i nie ma sposobu, aby wydedukować zmienną (1).
MSalters
To dobra uwaga, ale można ją łatwo przewyższyć, rzucając: Variable v1 ((double) 10)
jpinto3912
Zgadzam się, że czytelność kodu jest kwestią subiektywną, jednak w 100% zgadzam się z tym, co mówisz o specjalizacji szablonów. Rozwiązaniem prawdopodobnie byłoby podanie niezdefiniowanego błędu parametru szablonu (gdy kompilator spojrzy na specjalizację <int> i nie zobaczy żadnych prawidłowych konstruktorów, niech powie, że nie ma pojęcia, jakiego szablonu chcesz użyć i musisz wyraźnie określić), ale Zgadzam się, że to nie jest ładne rozwiązanie. Dodałbym to jako kolejną poważną lukę składniową, z którą należałoby się uporać (ale można by ją rozwiązać, gdyby przyjąć konsekwencje).
GRB
4
@ jpinto3912 - nie rozumiesz. Kompilator musi utworzyć instancję WSZYSTKICH możliwych zmiennych <T>, aby sprawdzić, czy ANY ctor Variable <T> :: Variable dostarcza niejednoznacznego ctora. Pozbycie się niejasności nie stanowi problemu - po prostu utwórz instancję zmiennej <double> samodzielnie, jeśli tego chcesz. To przede wszystkim znalezienie tej dwuznaczności, która to uniemożliwia.
MSalters
6

Standardy C ++ 03 i C ++ 11 nie pozwalają na wyprowadzanie argumentów szablonu z parametrów przekazanych do konstruktora.

Ale jest propozycja "Odliczenie parametrów szablonu dla konstruktorów", więc możesz otrzymać to, o co prosisz wkrótce. Edycja: rzeczywiście, ta funkcja została potwierdzona dla C ++ 17.

Zobacz: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3602.html i http://www.open-std.org/jtc1/sc22/wg21/docs/ dokumenty / 2015 / p0091r0.html

ChetS
źródło
Ta funkcja została dodana do C ++ 17, ale nie, jeśli „wkrótce” dotyczy przedziału czasowego od 6 do 8 lat. ;)
ChetS
2

Wiele klas nie zależy od parametrów konstruktora. Istnieje tylko kilka klas, które mają tylko jeden konstruktor i parametryzują na podstawie typów tego konstruktora.

Jeśli naprawdę potrzebujesz wnioskowania z szablonu, użyj funkcji pomocniczej:

template<typename obj>
class Variable 
{
      obj data;
public: 
      Variable(obj d)
      : data(d)
      { }
};

template<typename obj>
inline Variable<obj> makeVariable(const obj& d)
{
    return Variable<obj>(d);
}
rlbond
źródło
1
Oczywiście ta funkcjonalność okazałaby się użyteczna tylko dla niektórych klas, ale to samo można powiedzieć o wnioskowaniu funkcji. Nie wszystkie funkcje oparte na szablonach również pobierają swoje parametry z listy argumentów, jednak zezwalamy na wnioskowanie dla funkcji, które to robią.
GRB
1

Odliczanie typów jest ograniczone do funkcji szablonów w obecnym C ++, ale od dawna zdawano sobie sprawę, że odejmowanie typów w innych kontekstach byłoby bardzo przydatne. Stąd C ++ 0x auto.

Chociaż dokładnie to , co sugerujesz, nie będzie możliwe w C ++ 0x, poniższe pokazują, że możesz być całkiem blisko:

template <class X>
Variable<typename std::remove_reference<X>::type> MakeVariable(X&& x)
{
    // remove reference required for the case that x is an lvalue
    return Variable<typename std::remove_reference<X>::type>(std::forward(x));
}

void test()
{
    auto v = MakeVariable(2); // v is of type Variable<int>
}
James Hopkin
źródło
0

Masz rację, kompilator mógł z łatwością odgadnąć, ale o ile wiem, nie ma go w standardzie ani w C ++ 0x, więc będziesz musiał poczekać co najmniej 10 lat (stała szybkość zmian według standardów ISO), zanim dostawcy kompilatorów dodają tę funkcję

Robert Gould
źródło
To nie jest poprawne z nadchodzącym standardem, zostanie wprowadzone automatyczne słowo kluczowe. Spójrz na post Jamesa Hopkinsa w tym wątku. stackoverflow.com/questions/984394/… . Pokazuje, jak będzie to możliwe w C ++ 0x.
ovanes
1
Aby się poprawić, słowo kluczowe auto jest również obecne w obecnym standardzie, ale w innym celu.
ovanes
Wygląda na to, że minie 8 lat (od czasu tej odpowiedzi) ... więc 10 lat nie było złym domysłem, mimo że w międzyczasie obowiązywały dwa standardy!
Kyle Strand
-1

Przyjrzyjmy się problemowi w odniesieniu do klasy, którą każdy powinien znać - std :: vector.

Po pierwsze, bardzo częstym zastosowaniem wektora jest użycie konstruktora, który nie przyjmuje parametrów:

vector <int> v;

W tym przypadku oczywiście nie można wnioskować.

Drugim powszechnym zastosowaniem jest utworzenie wektora o ustalonej wielkości:

vector <string> v(100);

Tutaj, jeśli zastosowano wnioskowanie:

vector v(100);

otrzymujemy wektor int, a nie stringów, i przypuszczalnie nie ma rozmiaru!

Na koniec rozważ konstruktory, które pobierają wiele parametrów - z „wnioskiem”:

vector v( 100, foobar() );      // foobar is some class

Którego parametru należy użyć do wnioskowania? Musielibyśmy w jakiś sposób powiedzieć kompilatorowi, że powinien to być ten drugi.

Biorąc pod uwagę wszystkie te problemy dla klasy tak prostej jak wektor, łatwo zrozumieć, dlaczego nie używa się wnioskowania.


źródło
3
Myślę, że źle rozumiesz ten pomysł. Wnioskowanie o typie dla konstruktorów wystąpiłoby tylko wtedy, gdy typ szablonu jest częścią konstruktora. Załóżmy, że wektor ma szablon definicji szablonu <nazwa typu T>. Twój przykład nie stanowi problemu, ponieważ konstruktor wektora zostałby zdefiniowany jako wektor (rozmiar int), a nie wektor (rozmiar T). Jedynie w przypadku wektora (rozmiar T) wystąpiłoby jakiekolwiek wnioskowanie; w pierwszym przykładzie kompilator podałby błąd informujący, że T jest niezdefiniowane. Zasadniczo identyczne z działaniem wnioskowania z szablonu funkcji.
GRB
Więc miałoby to miejsce tylko dla konstruktorów, które mają jeden parametr i gdzie ten parametr jest typem parametru szablonu? Wydaje się, że jest to znikoma liczba przypadków.
Nie musi to być koniecznie pojedynczy parametr. Na przykład, można mieć konstruktor wektora wektora (int size, T firstElement). Jeśli szablon ma wiele parametrów (szablon <nazwa typu T, nazwa typu U>), można mieć Holder :: Holder (T firstObject, U secondObject). Jeśli szablon ma wiele parametrów, ale konstruktor przyjmuje tylko jeden z nich, np. Holder (U secondObject), wówczas T zawsze musiałoby być jawnie określone. Reguły miałyby być jak najbardziej podobne do wnioskowania z szablonu funkcji.
GRB
-2

Uczynienie z ctora szablonu Zmienna może mieć tylko jedną formę, ale różne ctory:

class Variable {
      obj data; // let the compiler guess
      public:
      template<typename obj>
      Variable(obj d)
       {
           data = d;
       }
};

int main()
{
    int num = 2;
    Variable var(num);  // Variable::data int?

    float num2 = 2.0f;
    Variable var2(num2);  // Variable::data float?
    return 0;         
}

Widzieć? Nie możemy mieć wielu członków Variable :: data.

Nick Dandoulakis
źródło
To nie miałoby sensu w żadnym scenariuszu. obj w kategoriach obj dane jest niezdefiniowane, ponieważ ta klasa nie jest już szablonem. Taki kod byłby nieważny w obu przypadkach.
GRB
Chciałem zachowania kompilatora, które opisujesz, więc znalazłem sposób na ominięcie tego ograniczenia (w moim przypadku), co może Cię zainteresować, stackoverflow.com/questions/228620/garbage-collection-in-c-why/ ...
Nick Dandoulakis
-2

Aby uzyskać więcej informacji na ten temat, zobacz The C ++ Template Argument Deduction .

Igor Krivokon
źródło
4
Czytałem ten artykuł wcześniej i wydawało się, że niewiele mówi o tym, co mówię. Pisarz wydaje się mówić o dedukcji argumentów w odniesieniu do zajęć tylko wtedy, gdy na początku artykułu mówi, że nie da się tego zrobić;) - gdybyś mógł wskazać sekcje, które uważasz za istotne, chociaż ja ' naprawdę to doceniam.
GRB