Mengapa tidak menyimpulkan parameter kerangka dari konstruktor?

102

pertanyaan saya hari ini cukup sederhana: mengapa kompilator tidak dapat menyimpulkan parameter template dari konstruktor kelas, sebanyak yang dapat dilakukan dari parameter fungsi? Misalnya, mengapa kode berikut tidak valid:

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
}

Seperti yang saya katakan, saya mengerti bahwa ini tidak valid, jadi pertanyaan saya adalah mengapa tidak? Akankah membiarkan ini menciptakan lubang sintaksis utama? Apakah ada contoh di mana orang tidak menginginkan fungsionalitas ini (di mana menyimpulkan suatu jenis akan menyebabkan masalah)? Saya hanya mencoba untuk memahami logika di balik mengizinkan inferensi template untuk fungsi, namun tidak untuk kelas yang dibuat dengan sesuai.

GRB
sumber
Saya akan mengundang seseorang (saya dan melakukannya, hanya tidak sekarang), untuk mengumpulkan jawaban Drahakar dan Pitis (setidaknya) sebagai contoh tandingan yang baik mengapa tidak berhasil
jpinto3912
2
Juga perhatikan bahwa ini mudah diselesaikan melaluitemplate<class T> Variable<T> make_Variable(T&& p) {return Variable<T>(std::forward<T>(p));}
Mooing Duck
3
Anda bisa mendapatkan apa yang Anda inginkan var = Variable <dectype (n)> (n);
QuentinUK
18
C ++ 17 akan mengizinkan ini! Proposal ini diterima: open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
underscore_d
1
@underscore_d Luar biasa! Tentang waktu! Rasanya wajar bagi saya begitulah cara kerjanya, dan sumber gangguan yang tidak dilakukannya.
amdn

Jawaban:

46

Saya pikir itu tidak valid karena konstruktor tidak selalu satu-satunya titik masuk kelas (saya berbicara tentang copy konstruktor dan operator =). Jadi misalkan Anda menggunakan kelas Anda seperti ini:

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

Saya tidak yakin apakah akan begitu jelas bagi parser untuk mengetahui jenis template apa MyClass pm;

Tidak yakin apakah yang saya katakan masuk akal tetapi jangan ragu untuk menambahkan komentar, itu pertanyaan yang menarik.

C ++ 17

Diterima bahwa C ++ 17 akan memiliki deduksi tipe dari argumen konstruktor.

Contoh:

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

Kertas yang diterima .

Drahakar
sumber
8
Ini sebenarnya adalah poin bagus yang tidak pernah saya pertimbangkan. Saya tidak melihat jalan lain di sekitar fakta bahwa pointer harus bertipe spesifik (yaitu harus MyClass <string> * pm). Jika itu kasusnya, maka yang Anda lakukan hanyalah menyelamatkan diri Anda sendiri dari menentukan jenis pada instantiation; hanya beberapa karakter kerja ekstra (dan hanya jika objek dibuat di tumpukan, bukan di heap, seperti di atas). Saya selalu curiga bahwa inferensi kelas dapat membuka kaleng sintaksis worm, dan saya pikir ini mungkin saja.
GRB
2
Saya tidak begitu mengerti bagaimana mengizinkan inferensi template-parameter dari konstruktor akan memerlukan mengizinkan deklarasi non-khusus tanpa panggilan konstruktor, seperti pada baris kedua Anda. Yaitu, di MyClass *pmsini tidak valid karena alasan yang sama bahwa fungsi yang dideklarasikan template <typename T> void foo();tidak dapat dipanggil tanpa spesialisasi eksplisit.
Kyle Strand
3
@KyleStrand Ya, dengan mengatakan 'argumen template kelas tidak dapat disimpulkan dari konstruktornya karena [contoh yang tidak menggunakan konstruktor apa pun] ', jawaban ini sama sekali tidak relevan. Saya benar-benar tidak percaya itu diterima, mencapai +29, membutuhkan 6 tahun bagi seseorang untuk memperhatikan masalah yang mencolok, dan duduk tanpa satu pun suara negatif selama 7 tahun. Apakah tidak ada orang lain yang berpikir saat mereka membaca, atau ...?
underscore_d
1
@underscore_d Saya suka bagaimana, seperti saat ini, jawaban ini mengatakan "mungkin ada beberapa masalah dengan proposal ini; Saya tidak yakin apakah yang baru saya katakan masuk akal (!), silakan berkomentar (!!); dan oh ngomong-ngomong, inilah cara kerja C ++ 17. "
Kyle Strand
1
@KyleStrand Ah ya, itu masalah lain, yang saya perhatikan tetapi lupa menyebutkan di antara semua kesenangan lainnya. Pengeditan tentang C ++ 17 bukan oleh OP ... dan IMO seharusnya tidak disetujui, tetapi diposting sebagai jawaban baru: itu akan dapat dideklinasikan sebagai 'mengubah arti posting' bahkan jika posting itu memiliki tidak ada artinya untuk memulai ... Saya tidak menyadari pengeditan di bagian yang sama sekali baru adalah permainan yang adil dan tentu saja pengeditan yang kurang drastis ditolak, tapi saya rasa itulah keberuntungan pengundian dalam hal pengulas yang Anda dapatkan.
underscore_d
27

Anda tidak dapat melakukan apa yang Anda minta karena alasan orang lain menjawab, tetapi Anda dapat melakukan ini:

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

yang untuk semua maksud dan tujuan adalah hal yang sama yang Anda minta. Jika Anda menyukai enkapsulasi, Anda dapat menjadikan make_variable sebagai fungsi anggota statis. Itulah yang disebut orang dengan nama konstruktor. Jadi tidak hanya melakukan apa yang Anda inginkan, tetapi juga hampir disebut apa yang Anda inginkan: kompilator menyimpulkan parameter template dari konstruktor (bernama).

NB: kompiler yang masuk akal akan mengoptimalkan objek sementara ketika Anda menulis sesuatu seperti

auto v = make_variable(instance);
Lionel
sumber
6
Ingin menunjukkan bahwa tidak terlalu berguna untuk membuat fungsi anggota statis dalam kasus seperti itu karena untuk itu Anda harus menentukan argumen template untuk kelas agar tetap memanggilnya sehingga tidak ada gunanya menyimpulkannya.
Predelnik
3
Dan bahkan lebih baik lagi di C ++ 11 Anda dapat melakukannya auto v = make_variable(instance)sehingga Anda tidak benar-benar harus menentukan tipenya
Claudiu
1
Ya, lol pada gagasan untuk menyatakan fungsi make sebagai staticanggota ... pikirkan itu sebentar lagi. Bahwa selain: fungsi make bebas memang yang solusi, tapi itu banyak boilerplate berlebihan, bahwa sementara Anda sedang mengetik, Anda hanya tahu Anda tidak harus karena compiler memiliki akses ke semua informasi yang Anda mengulangi. .. dan untungnya C ++ 17 mengkanonisasinya.
underscore_d
21

Di era pencerahan tahun 2016, dengan dua standar baru di bawah ikat pinggang kami sejak pertanyaan ini diajukan dan yang baru sudah dekat, hal penting untuk diketahui adalah bahwa kompiler yang mendukung standar C ++ 17 akan mengkompilasi kode Anda apa adanya .

Pengurangan argumen template untuk template kelas di C ++ 17

Di sini (hasil edit oleh Olzhas Zhumabek dari jawaban yang diterima) adalah makalah yang merinci perubahan yang relevan dengan standar.

Mengatasi masalah dari jawaban lain

Jawaban peringkat teratas saat ini

Jawaban ini menunjukkan bahwa "copy konstruktor dan operator=" tidak akan mengetahui spesialisasi template yang benar.

Ini tidak masuk akal, karena konstruktor salinan standar dan operator= hanya ada untuk jenis template yang dikenal :

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;

Di sini, seperti yang saya catat di komentar, tidak ada alasan untuk MyClass *pmmenjadi deklarasi hukum dengan atau tanpa bentuk inferensi baru: MyClass bukan tipe (ini template), jadi tidak masuk akal untuk mendeklarasikan pointer dari tipe MyClass. Berikut salah satu cara yang mungkin untuk memperbaiki contoh:

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

Berikut, pmadalah sudah dari jenis yang tepat, dan kesimpulan yang sepele. Selain itu, tidak mungkin untuk mencampur tipe secara tidak sengaja saat memanggil konstruktor salinan:

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

Di sini, pmakan menjadi penunjuk ke salinan m. Di sini, MyClasssedang dikonstruksi dari m—yang bertipe MyClass<string>(dan bukan tipe yang tidak ada MyClass). Dengan demikian, pada titik di mana pmtipe 's disimpulkan, ada adalah informasi yang cukup untuk mengetahui bahwa template-jenis m, dan karena itu template-jenis pm, yangstring .

Selain itu, berikut ini akan selalu memunculkan kesalahan kompilasi :

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

Ini karena deklarasi konstruktor salinan tidak memiliki template:

MyClass(const MyClass&);

Di sini, tipe template argumen copy-constructor cocok dengan tipe template kelas secara keseluruhan; yaitu, saat MyClass<string>dibuat instance, MyClass<string>::MyClass(const MyClass<string>&);dibuat instance-nya, dan ketika MyClass<int>dibuat, MyClass<int>::MyClass(const MyClass<int>&);dibuat instance-nya. Kecuali ditentukan secara eksplisit atau konstruktor templatized dideklarasikan, tidak ada alasan bagi compiler untuk membuat instanceMyClass<int>::MyClass(const MyClass<string>&); , yang jelas tidak sesuai.

Jawabannya oleh Cătălin Pitiș

Pitiș memberikan contoh deduksi Variable<int>dan Variable<double>, kemudian menyatakan:

Saya memiliki nama jenis yang sama (Variabel) dalam kode untuk dua jenis yang berbeda (Variabel dan Variabel). Dari sudut pandang subjektif saya, ini mempengaruhi keterbacaan kode cukup banyak.

Seperti disebutkan dalam contoh sebelumnya, Variableitu sendiri bukanlah nama tipe, meskipun fitur baru membuatnya terlihat seperti nama secara sintaksis.

Pitiș kemudian menanyakan apa yang akan terjadi jika tidak ada konstruktor yang diberikan yang mengizinkan inferensi yang sesuai. Jawabannya adalah tidak ada inferensi yang diizinkan, karena inferensi dipicu oleh panggilan konstruktor . Tanpa panggilan konstruktor, tidak ada inferensi .

Ini mirip dengan menanyakan versi apa fooyang disimpulkan di sini:

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

Jawabannya adalah kode ini ilegal, karena alasan yang disebutkan.

Jawaban MSalter

Ini, sejauh yang saya tahu, satu-satunya jawaban untuk memunculkan kekhawatiran yang sah tentang fitur yang diusulkan.

Contohnya adalah:

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

Pertanyaan kuncinya adalah, apakah kompilator memilih konstruktor tipe-tersirat di sini atau konstruktor salinan ?

Mencoba kodenya, kita dapat melihat bahwa konstruktor salinan dipilih. Untuk memperluas contoh :

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

Saya tidak yakin bagaimana proposal dan versi baru standar menentukan ini; tampaknya ditentukan oleh "panduan deduksi," yang merupakan sedikit bahasa standar baru yang saya belum mengerti.

Saya juga tidak yakin mengapa var4pemotongan tersebut ilegal; kesalahan kompilator dari g ++ tampaknya menunjukkan bahwa pernyataan tersebut sedang diurai sebagai deklarasi fungsi.

Kyle Strand
sumber
Jawaban yang sangat bagus dan terperinci! var4hanya kasus "parse paling menjengkelkan" (tidak terkait dengan pengurangan argumen template). Kami dulu hanya menggunakan tanda kurung ekstra untuk ini, tetapi belakangan ini saya pikir menggunakan kawat gigi untuk menunjukkan konstruksi adalah saran yang biasa.
Sumudu Fernando
@SumuduFernando Terima kasih! Apakah maksud Anda itu Variable var4(Variable(num));diperlakukan sebagai deklarasi fungsi? Jika ya, mengapa Variable(num)spesifikasi parameter yang valid?
Kyle Strand
@SumuduFernando Tidak apa-apa, saya tidak tahu ini valid: coliru.stacked-crooked.com/a/98c36b8082660941
Kyle Strand
11

Masih hilang: Itu membuat kode berikut cukup ambigu:

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
sumber
Poin bagus lainnya. Dengan asumsi bahwa ada variabel yang didefinisikan konstruktor salinan (Variabel <obj> d), harus ada semacam prioritas yang ditetapkan.
GRB
1
Atau, sebagai alternatif, minta kompilator melontarkan kesalahan parameter template yang tidak ditentukan lagi, seperti yang saya sarankan sehubungan dengan jawaban Pitis. Namun, jika Anda mengambil rute tersebut, frekuensi terjadinya inferensi tanpa masalah (kesalahan) semakin kecil.
GRB
Ini sebenarnya adalah poin yang menarik, dan (seperti yang saya catat dalam jawaban saya) saya belum yakin bagaimana proposal C ++ 17 yang diterima menyelesaikan masalah ini.
Kyle Strand
9

Misalkan kompilator mendukung apa yang Anda minta. Maka kode ini valid:

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

// Some code here

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

Sekarang, saya memiliki nama jenis yang sama (Variabel) dalam kode untuk dua jenis yang berbeda (Variabel dan Variabel). Dari sudut pandang subjektif saya, ini mempengaruhi keterbacaan kode cukup banyak. Memiliki nama jenis yang sama untuk dua jenis yang berbeda dalam namespace yang sama tampak menyesatkan bagi saya.

Pembaruan nanti: Hal lain yang perlu dipertimbangkan: spesialisasi template sebagian (atau penuh).

Bagaimana jika saya mengkhususkan Variabel dan tidak memberikan konstruktor seperti yang Anda harapkan?

Jadi saya ingin:

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

Lalu saya punya kode:

Variable v( 10);

Apa yang harus dilakukan kompilator? Gunakan definisi kelas Variabel generik untuk menyimpulkan bahwa itu adalah Variabel, lalu temukan bahwa Variabel tidak menyediakan satu konstruktor parameter?

Cătălin Pitiș
sumber
1
Lebih buruk lagi: bagaimana jika Anda hanya memiliki Variable <int> :: Variable (float)? Anda sekarang memiliki dua cara untuk menyimpulkan Variabel (1f) dan tidak ada cara untuk menyimpulkan Variabel (1).
MSalters
Ini poin yang bagus, tetapi bisa dengan mudah dilampaui dengan mentransmisikan: Variabel v1 ((double) 10)
jpinto3912
Saya setuju keterbacaan kode adalah masalah subjektif, namun, saya setuju 100% dengan apa yang Anda katakan tentang spesialisasi template. Solusinya mungkin akan memberikan kesalahan parameter template tidak terdefinisi (setelah kompilator melihat spesialisasi <int> dan tidak melihat konstruktor yang valid, katakanlah ia tidak tahu template apa yang ingin Anda gunakan dan yang harus Anda tentukan secara eksplisit) tetapi Saya setuju bahwa ini bukanlah solusi yang bagus. Saya akan menambahkan ini sebagai lubang sintaksis utama lain yang perlu ditangani (tetapi dapat diselesaikan jika seseorang menerima konsekuensinya).
GRB
4
@ jpinto3912 - Anda melewatkan intinya. Kompilator harus membuat instance SEMUA Variabel <T> yang memungkinkan untuk memeriksa apakah ANY ctor Variable <T> :: Variable menyediakan ctor yang ambigu. Menyingkirkan ambiguitas bukanlah masalah - sederhana membuat instance Variabel <double> sendiri jika itu yang Anda inginkan. Menemukan ambiguitas itu di tempat pertama yang membuatnya tidak mungkin.
MSalters
6

C ++ 03 dan standar C ++ 11 tidak mengizinkan pengurangan argumen template dari parameter yang diteruskan ke konsturuktor.

Tapi ada proposal untuk "Pengurangan parameter template untuk konstruktor" sehingga Anda bisa mendapatkan apa yang Anda minta segera. Sunting: memang, fitur ini telah dikonfirmasi untuk C ++ 17.

Lihat: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3602.html dan http://www.open-std.org/jtc1/sc22/wg21/docs/ makalah / 2015 / p0091r0.html

ChetS
sumber
Fitur tersebut telah ditambahkan ke C ++ 17, tetapi tidak jika "segera" berlaku untuk jangka waktu 6 hingga 8 tahun. ;)
ChetS
2

Banyak kelas tidak bergantung pada parameter konstruktor. Hanya ada beberapa kelas yang hanya memiliki satu konstruktor, dan melakukan parameterisasi berdasarkan jenis konstruktor ini.

Jika Anda benar-benar membutuhkan inferensi template, gunakan fungsi helper:

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.dll
sumber
1
Tentu saja fungsionalitas ini hanya terbukti berguna untuk beberapa kelas, tetapi hal yang sama dapat dikatakan untuk inferensi fungsi. Tidak semua fungsi template mengambil parameternya dari daftar argumen, namun kami mengizinkan inferensi untuk fungsi yang melakukannya.
GRB
1

Pengurangan tipe terbatas pada fungsi template di C ++ saat ini, tetapi telah lama disadari bahwa pengurangan tipe dalam konteks lain akan sangat berguna. Oleh karena itu C ++ 0x auto.

Sementara persis apa yang Anda sarankan tidak akan mungkin di C ++ 0x, menunjukkan berikut Anda bisa mendapatkan cukup dekat:

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
sumber
0

Anda benar, kompiler dapat dengan mudah menebaknya, tetapi itu tidak dalam standar atau C ++ 0x sejauh yang saya tahu sehingga Anda harus menunggu setidaknya 10 tahun lagi (standar ISO fixed turn around rate) sebelum penyedia compiller menambahkan fitur ini

Robert Gould
sumber
Itu tidak benar dengan standar yang akan datang kata kunci otomatis akan diperkenalkan. Lihatlah pos James Hopkins di utas ini. stackoverflow.com/questions/984394/… . Dia menunjukkan bagaimana hal itu bisa dilakukan dalam C ++ 0x.
ovanes
1
Sekadar mengoreksi diri sendiri, kata kunci otomatis juga ada dalam standar saat ini, tetapi untuk tujuan yang berbeda.
ovanes
Sepertinya itu akan menjadi 8 tahun (dari saat jawaban ini) ... jadi 10 tahun bukanlah tebakan yang buruk, meskipun saat ini ada dua standar!
Kyle Strand
-1

Mari kita lihat masalah dengan mengacu pada kelas yang harus dipahami semua orang dengan - std :: vector.

Pertama, penggunaan vektor yang paling umum adalah menggunakan konstruktor yang tidak menggunakan parameter:

vector <int> v;

Dalam kasus ini, jelas tidak ada kesimpulan yang dapat dilakukan.

Penggunaan umum kedua adalah membuat vektor berukuran sebelumnya:

vector <string> v(100);

Di sini, jika inferensi digunakan:

vector v(100);

kita mendapatkan vektor int, bukan string, dan mungkin ukurannya tidak!

Terakhir, pertimbangkan konstruktor yang mengambil beberapa parameter - dengan "inferensi":

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

Parameter mana yang harus digunakan untuk inferensi? Kita memerlukan beberapa cara untuk memberi tahu kompilator bahwa itu harus yang kedua.

Dengan semua masalah ini untuk kelas yang sederhana seperti vektor, mudah untuk melihat mengapa inferensi tidak digunakan.


sumber
3
Saya pikir Anda salah paham tentang gagasan itu. Jenis inferensi untuk konstruktor hanya akan terjadi JIKA jenis template adalah bagian dari konstruktor. Asumsikan bahwa vektor memiliki template definisi template <typename T>. Contoh Anda tidak menjadi masalah karena konstruktor vektor akan didefinisikan sebagai vektor (ukuran int), bukan vektor (ukuran T). Hanya dalam kasus vektor (ukuran T) akan terjadi inferensi; pada contoh pertama, kompilator akan memberikan kesalahan yang mengatakan bahwa T tidak terdefinisi. Pada dasarnya identik dengan cara kerja inferensi template fungsi.
GRB
Jadi ini hanya akan terjadi untuk konstruktor yang memiliki satu parameter dan di mana parameter itu adalah jenis parameter template? Tampaknya itu hanya sejumlah kecil contoh.
Ini tidak harus berupa parameter tunggal. Misalnya, seseorang dapat memiliki konstruktor vektor dari vektor (int size, T firstElement). Jika sebuah template memiliki beberapa parameter (template <typename T, typename U>), seseorang dapat memiliki Holder :: Holder (T firstObject, U secondObject). Jika template memiliki beberapa parameter tetapi konstruktor hanya mengambil salah satunya, misalnya Holder (U secondObject), maka T harus selalu dinyatakan secara eksplisit. Aturan akan dimaksudkan agar semirip mungkin dengan inferensi template fungsi.
GRB
-2

Membuat ctor sebagai template Variabel hanya dapat memiliki satu formulir tetapi beragam ctor:

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;         
}

Lihat? Kita tidak bisa memiliki beberapa Variable :: data members.

Nick Dandoulakis
sumber
Itu tidak masuk akal dalam skenario apa pun. obj dalam hal data obj tidak terdefinisi karena kelas itu bukan lagi template. Kode seperti itu akan menjadi tidak valid.
GRB
Saya ingin perilaku kompilator yang Anda gambarkan, jadi saya mencari cara untuk melewati batasan itu (dalam kasus saya), yang mungkin menarik bagi Anda, stackoverflow.com/questions/228620/garbage-collection-in-c-why/…
Nick Dandoulakis
-2

Lihat Pemotongan Argumen C ++ Template untuk info lebih lanjut tentang ini.

Igor Krivokon
sumber
4
Saya membaca artikel ini sebelumnya dan sepertinya tidak banyak berbicara tentang apa yang saya katakan. Satu-satunya saat penulis tampaknya berbicara tentang pengurangan argumen sehubungan dengan kelas adalah ketika dia mengatakan itu tidak dapat dilakukan di bagian atas artikel;) - jika Anda dapat menunjukkan bagian yang menurut Anda relevan meskipun saya ' d sangat menghargai itu.
GRB