Templat C ++ yang hanya menerima tipe tertentu

159

Di Java, Anda dapat menentukan kelas generik yang hanya menerima tipe yang memperluas kelas pilihan Anda, misalnya:

public class ObservableList<T extends List> {
  ...
}

Ini dilakukan dengan menggunakan kata kunci "extends".

Apakah ada yang setara dengan kata kunci ini di C ++?

mgamer
sumber
pertanyaan yang sudah cukup lama ... Saya merasa apa yang hilang di sini (juga dari jawaban) adalah bahwa generik Java tidak benar-benar setara dengan templat di C ++. Ada kesamaan, tetapi saya harus berhati-hati dengan langsung menerjemahkan solusi java ke C ++ hanya untuk menyadari bahwa mereka mungkin dibuat untuk berbagai jenis masalah;)
idclev 463035818

Jawaban:

104

Saya sarankan menggunakan fitur penegasan statis Boost dalam konser dengan is_base_ofdari perpustakaan Boost Type Traits:

template<typename T>
class ObservableList {
    BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
    ...
};

Dalam beberapa kasus lain yang lebih sederhana, Anda dapat dengan mudah mendeklarasikan templat global, tetapi hanya mendefinisikan (secara khusus atau sebagian mengkhususkan) untuk jenis yang valid:

template<typename T> class my_template;     // Declare, but don't define

// int is a valid type
template<> class my_template<int> {
    ...
};

// All pointer types are valid
template<typename T> class my_template<T*> {
    ...
};

// All other types are invalid, and will cause linker error messages.

[Minor EDIT 6/12/2013: Menggunakan template yang dideklarasikan tetapi tidak didefinisikan akan menghasilkan pesan kesalahan linker , bukan compiler.]

j_random_hacker
sumber
Pernyataan statis juga bagus. :)
macbirdie
5
@ John: Saya khawatir spesialisasi hanya akan sama myBaseTypepersis. Sebelum menolak Boost, Anda harus tahu bahwa sebagian besar hanya kode templat header - jadi tidak ada memori atau biaya waktu saat runtime untuk hal-hal yang tidak Anda gunakan. Juga hal-hal tertentu yang akan Anda gunakan di sini ( BOOST_STATIC_ASSERT()dan is_base_of<>) dapat diimplementasikan hanya dengan menggunakan deklarasi (yaitu tidak ada definisi aktual dari fungsi atau variabel) sehingga mereka tidak akan mengambil ruang atau waktu juga.
j_random_hacker
50
C ++ 11 telah tiba. Sekarang bisa kita gunakan static_assert(std::is_base_of<List, T>::value, "T must extend list").
Siyuan Ren
2
BTW, alasan kurung ganda diperlukan adalah bahwa BOOST_STATIC_ASSERT adalah makro dan kurung tambahan mencegah preprocessor dari menafsirkan koma dalam argumen fungsi is_base_of sebagai argumen makro ke-2.
jfritz42
1
@Andreyua: Saya tidak begitu mengerti apa yang hilang. Anda bisa mencoba mendeklarasikan variabel my_template<int> x;atau my_template<float**> y;memverifikasi bahwa kompiler mengizinkannya, dan kemudian mendeklarasikan variabel my_template<char> z;dan memverifikasi tidak.
j_random_hacker
134

Ini biasanya tidak beralasan dalam C ++, seperti yang dicatat oleh jawaban lain di sini. Dalam C ++ kita cenderung mendefinisikan tipe generik berdasarkan batasan lain selain "inherit from this class". Jika Anda benar-benar ingin melakukan itu, cukup mudah dilakukan di C ++ 11 dan <type_traits>:

#include <type_traits>

template<typename T>
class observable_list {
    static_assert(std::is_base_of<list, T>::value, "T must inherit from list");
    // code here..
};

Ini memecah banyak konsep yang orang harapkan dalam C ++. Lebih baik menggunakan trik seperti mendefinisikan sifat Anda sendiri. Misalnya, mungkin observable_listingin menerima segala jenis wadah yang memiliki typedefs const_iteratordan abegin dan endfungsi anggota yang kembali const_iterator. Jika Anda membatasi ini untuk kelas yang mewarisi dari listmaka pengguna yang memiliki tipe mereka sendiri yang tidak mewarisi dari listtetapi menyediakan fungsi-fungsi anggota ini dan mengetik tidak akan dapat menggunakan Anda observable_list.

Ada dua solusi untuk masalah ini, salah satunya adalah dengan tidak membatasi apa pun dan mengandalkan mengetik bebek. Masalah besar dari solusi ini adalah melibatkan sejumlah besar kesalahan yang sulit bagi pengguna untuk melakukan grok. Solusi lain adalah mendefinisikan ciri-ciri untuk membatasi jenis yang disediakan untuk memenuhi persyaratan antarmuka. Masalah besar untuk solusi ini adalah yang melibatkan penulisan tambahan yang dapat dilihat sebagai menjengkelkan. Namun, sisi positifnya adalah Anda akan dapat menulis pesan kesalahan Anda sendiri static_assert.

Untuk kelengkapan, solusi untuk contoh di atas diberikan:

#include <type_traits>

template<typename...>
struct void_ {
    using type = void;
};

template<typename... Args>
using Void = typename void_<Args...>::type;

template<typename T, typename = void>
struct has_const_iterator : std::false_type {};

template<typename T>
struct has_const_iterator<T, Void<typename T::const_iterator>> : std::true_type {};

struct has_begin_end_impl {
    template<typename T, typename Begin = decltype(std::declval<const T&>().begin()),
                         typename End   = decltype(std::declval<const T&>().end())>
    static std::true_type test(int);
    template<typename...>
    static std::false_type test(...);
};

template<typename T>
struct has_begin_end : decltype(has_begin_end_impl::test<T>(0)) {};

template<typename T>
class observable_list {
    static_assert(has_const_iterator<T>::value, "Must have a const_iterator typedef");
    static_assert(has_begin_end<T>::value, "Must have begin and end member functions");
    // code here...
};

Ada banyak konsep yang ditunjukkan pada contoh di atas yang menampilkan fitur C ++ 11. Beberapa istilah pencarian untuk yang penasaran adalah templat variadic, SFINAE, ekspresi SFINAE, dan ketik ciri-ciri.

Rapptz
sumber
2
Saya tidak pernah menyadari C ++ templat menggunakan bebek mengetik hingga hari ini. Agak aneh!
Andy
2
Mengingat kendala kebijakan yang luas yang diperkenalkan oleh C ++ kepada C , tidak yakin mengapa template<class T:list>konsep tersebut menyinggung. Terima kasih atas tipnya.
bvj
61

Solusi sederhana, yang belum ada yang disebutkan, adalah mengabaikan masalahnya. Jika saya mencoba menggunakanint tipe templat sebagai templat fungsi yang mengharapkan kelas kontainer seperti vektor atau daftar, maka saya akan mendapatkan kesalahan kompilasi. Mentah dan sederhana, tetapi itu memecahkan masalah. Compiler akan mencoba menggunakan tipe yang Anda tentukan, dan jika gagal, itu menghasilkan kesalahan kompilasi.

Satu-satunya masalah dengan itu adalah bahwa pesan kesalahan yang Anda dapatkan akan sulit dibaca. Meskipun demikian, ini adalah cara yang sangat umum untuk melakukan ini. Pustaka standar penuh dengan templat fungsi atau kelas yang mengharapkan perilaku tertentu dari tipe templat, dan tidak melakukan apa pun untuk memeriksa apakah jenis yang digunakan valid.

Jika Anda ingin pesan kesalahan yang lebih bagus (atau jika Anda ingin menangkap kasus yang tidak menghasilkan kesalahan kompiler, tetapi masih tidak masuk akal) Anda bisa, tergantung pada seberapa kompleks Anda ingin membuatnya, gunakan salah satu penambah statik Boost atau perpustakaan Boost concept_check.

Dengan kompiler terkini Anda memiliki built_in static_assert, yang bisa digunakan sebagai gantinya.

jalf
sumber
7
Ya, saya selalu berpikir bahwa templat adalah hal yang paling dekat dengan mengetik bebek di C ++. Jika memiliki semua elemen yang diperlukan untuk templat, ia dapat digunakan dalam templat.
@ John: Maaf, saya tidak bisa membuat kepala atau ekor itu. Jenis yang mana T, dan dari mana kode ini disebut? Tanpa beberapa konteks, saya tidak memiliki kesempatan untuk memahami potongan kode itu. Tapi apa yang saya katakan itu benar. Jika Anda mencoba memanggil toString()tipe yang tidak memiliki toStringfungsi anggota, maka Anda akan mendapatkan kesalahan kompilasi.
jalf
@ John: lain kali, mungkin Anda harus menjadi orang yang kurang memicu-senang downvoting ketika masalah ada dalam kode Anda
jalf
@jalf, ok. +1. Ini adalah jawaban yang bagus hanya berusaha menjadikannya yang terbaik. Maaf telah salah membaca. Saya pikir kami berbicara tentang menggunakan tipe sebagai parameter untuk kelas bukan untuk templat fungsi, yang saya kira adalah anggota yang sebelumnya tetapi perlu meminta compiler untuk menandai.
John
13

Kita dapat menggunakan std::is_base_ofdan std::enable_if:
( static_assertdapat dihapus, kelas-kelas di atas dapat diimplementasikan secara kustom atau digunakan dari dorongan jika kita tidak dapat referensi type_traits)

#include <type_traits>
#include <list>

class Base {};
class Derived: public Base {};

#if 0   // wrapper
template <class T> class MyClass /* where T:Base */ {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
    typename std::enable_if<std::is_base_of<Base, T>::value, T>::type inner;
};
#elif 0 // base class
template <class T> class MyClass: /* where T:Base */
    protected std::enable_if<std::is_base_of<Base, T>::value, T>::type {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
};
#elif 1 // list-of
template <class T> class MyClass /* where T:list<Base> */ {
    static_assert(std::is_base_of<Base, typename T::value_type>::value , "T::value_type is not derived from Base");
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type base; 
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type::value_type value_type;

};
#endif

int main() {
#if 0   // wrapper or base-class
    MyClass<Derived> derived;
    MyClass<Base> base;
//  error:
    MyClass<int> wrong;
#elif 1 // list-of
    MyClass<std::list<Derived>> derived;
    MyClass<std::list<Base>> base;
//  error:
    MyClass<std::list<int>> wrong;
#endif
//  all of the static_asserts if not commented out
//  or "error: no type named ‘type’ in ‘struct std::enable_if<false, ...>’ pointing to:
//  1. inner
//  2. MyClass
//  3. base + value_type
}
firda
sumber
13

Sejauh yang saya tahu ini saat ini tidak mungkin di C ++. Namun, ada rencana untuk menambahkan fitur yang disebut "konsep" dalam standar C ++ 0x baru yang menyediakan fungsionalitas yang Anda cari. Ini artikel Wikipedia tentang C ++ Konsep akan menjelaskannya lebih rinci.

Saya tahu ini tidak memperbaiki masalah langsung Anda tetapi ada beberapa kompiler C ++ yang sudah mulai menambahkan fitur dari standar baru, jadi mungkin saja untuk menemukan kompiler yang telah mengimplementasikan fitur konsep.

Barry Carr
sumber
4
Sayangnya konsep telah dikeluarkan dari standar.
macbirdie
4
Kendala dan konsep harus diadopsi untuk C ++ 20.
Petr Javorik
Mungkin bahkan tanpa konsep, menggunakan static_assertdan SFINAE, seperti yang ditunjukkan oleh jawaban lainnya. Masalah yang tersisa untuk seseorang yang berasal dari Java atau C #, atau Haskell (...) adalah bahwa kompiler C ++ 20 tidak melakukan pengecekan definisi terhadap konsep yang diperlukan, yang dilakukan oleh Java dan C #.
user7610
10

Saya pikir semua jawaban sebelumnya telah kehilangan pandangan tentang hutan untuk pepohonan.

Java generics tidak sama dengan templat ; mereka menggunakan tipe erasure , yang merupakan teknik dinamis , daripada mengkompilasi polimorfisme waktu , yang merupakan teknik statis . Seharusnya jelas mengapa kedua taktik yang sangat berbeda ini tidak berhasil dengan baik.

Daripada mencoba menggunakan konstruksi waktu kompilasi untuk mensimulasikan proses waktu berjalan, mari kita lihat apa yang extendssebenarnya dilakukan: menurut Stack Overflow dan Wikipedia , extends digunakan untuk mengindikasikan subklasifikasi.

C ++ juga mendukung subclassing.

Anda juga menunjukkan kelas kontainer, yang menggunakan penghapusan tipe dalam bentuk generik, dan diperluas untuk melakukan pemeriksaan tipe. Di C ++, Anda harus melakukan sendiri mesin tipe erasure, yang sederhana: buat pointer ke superclass.

Mari kita bungkus menjadi typedef, untuk membuatnya lebih mudah digunakan, daripada membuat seluruh kelas, dan voila:

typedef std::list<superclass*> subclasses_of_superclass_only_list;

Sebagai contoh:

class Shape { };
class Triangle : public Shape { };

typedef std::list<Shape*> only_shapes_list;
only_shapes_list shapes;

shapes.push_back(new Triangle()); // Works, triangle is kind of shape
shapes.push_back(new int(30)); // Error, int's are not shapes

Sekarang, tampaknya List adalah sebuah antarmuka, mewakili semacam koleksi. Antarmuka dalam C ++ hanya akan menjadi kelas abstrak, yaitu, kelas yang mengimplementasikan apa pun kecuali metode virtual murni. Dengan menggunakan metode ini, Anda dapat dengan mudah mengimplementasikan contoh java Anda di C ++, tanpa Konsep atau spesialisasi templat apa pun. Ini juga akan berkinerja lambat seperti generik gaya Java karena tampilan tabel virtual, tetapi ini sering dapat menjadi kerugian yang dapat diterima.

Alice
sumber
3
Saya bukan penggemar jawaban yang menggunakan frasa seperti "itu harus jelas," atau "semua orang tahu", dan kemudian menjelaskan apa yang jelas atau dikenal secara universal. Jelas itu relatif terhadap konteks, pengalaman dan konteks pengalaman. Pernyataan seperti itu pada dasarnya tidak sopan.
3Dave
2
@DavidLively Sudah terlambat sekitar dua tahun untuk mengkritik jawaban ini karena etiket, tetapi saya juga tidak setuju dengan Anda dalam contoh khusus ini; Saya menjelaskan mengapa kedua teknik itu tidak berjalan bersama sebelum menyatakan itu jelas, bukan setelahnya. Saya memberikan konteksnya, dan kemudian mengatakan kesimpulan dari konteks itu jelas. Itu tidak persis cocok dengan cetakan Anda.
Alice
Penulis jawaban ini mengatakan sesuatu yang jelas setelah melakukan beberapa pengangkatan berat. Saya tidak berpikir penulis bermaksud mengatakan solusinya jelas.
Luke Gehorsam
10

Persamaan yang hanya menerima tipe T yang berasal dari tipe Daftar sepertinya

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};
nh_
sumber
8

Ringkasan eksekutif: Jangan lakukan itu.

Jawaban j_random_hacker memberi tahu Anda cara melakukan ini. Namun, saya juga ingin menunjukkan bahwa Anda tidak boleh melakukan ini. Inti dari semua templat adalah mereka dapat menerima jenis apa pun yang kompatibel, dan batasan tipe gaya Java mematahkannya.

Batasan tipe Java adalah bug, bukan fitur. Mereka ada di sana karena Java tidak mengetik penghapusan pada obat generik, jadi Java tidak dapat menemukan cara memanggil metode berdasarkan nilai parameter tipe saja.

C ++ di sisi lain tidak memiliki batasan seperti itu. Tipe parameter template dapat tipe apa saja yang kompatibel dengan operasi yang digunakan. Tidak harus ada kelas dasar yang sama. Ini mirip dengan "Duck Typing," Python, tetapi dilakukan pada waktu kompilasi.

Contoh sederhana yang menunjukkan kekuatan templat:

// Sum a vector of some type.
// Example:
// int total = sum({1,2,3,4,5});
template <typename T>
T sum(const vector<T>& vec) {
    T total = T();
    for (const T& x : vec) {
        total += x;
    }
    return total;
}

Fungsi jumlah ini dapat menjumlahkan vektor dari jenis apa pun yang mendukung operasi yang benar. Ia bekerja dengan kedua primitif seperti int / long / float / double, dan tipe numerik yang ditentukan pengguna yang membebani operator + =. Heck, Anda bahkan dapat menggunakan fungsi ini untuk bergabung dengan string, karena mereka mendukung + =.

Tidak diperlukan tinju / unboxing primitif.

Perhatikan bahwa itu juga membangun contoh baru T menggunakan T (). Ini sepele di C ++ menggunakan antarmuka implisit, tetapi tidak benar-benar mungkin di Jawa dengan kendala tipe.

Sementara template C ++ tidak memiliki batasan tipe eksplisit, mereka masih tipe aman, dan tidak akan dikompilasi dengan kode yang tidak mendukung operasi yang benar.

lumba-lumba
sumber
2
Jika Anda menyarankan templat yang tidak terspesialisasi, dapatkah Anda juga menjelaskan mengapa ia menggunakan bahasa?
1
Saya mengerti maksud Anda, tetapi jika argumen templat Anda harus berasal dari jenis tertentu, maka lebih baik memiliki pesan yang mudah diinterpretasikan dari static_assert daripada muntah kesalahan kompiler normal.
jhoffman0x
1
Ya, C ++ lebih ekspresif di sini, tetapi sementara itu umumnya hal yang baik (karena kita dapat mengekspresikan lebih banyak dengan lebih sedikit), kadang-kadang kita ingin secara sengaja membatasi kekuatan yang kita berikan kepada diri kita sendiri, untuk mendapatkan kepastian bahwa kita sepenuhnya memahami suatu sistem.
j_random_hacker
Jenis @Curg spesialisasi berguna ketika Anda ingin dapat mengambil keuntungan dari beberapa hal yang hanya dapat dilakukan untuk jenis tertentu. misalnya, boolean ~ biasanya ~ masing-masing satu byte, meskipun satu byte ~ biasanya ~ dapat menampung 8 bit / boolean; kelas koleksi templat dapat (dan dalam kasus std :: map) mengkhususkan untuk boolean sehingga dapat mengemas data lebih ketat untuk menghemat memori.
thecoshman
Juga, untuk memperjelas, jawaban ini tidak mengatakan "jangan pernah mengkhususkan template" itu mengatakan jangan gunakan fitur itu untuk mencoba membatasi jenis apa yang dapat digunakan dengan template.
thecoshman
6

Itu tidak mungkin di C ++ biasa, tetapi Anda dapat memverifikasi parameter template pada waktu kompilasi melalui Pengecekan Konsep, misalnya menggunakan BCCL Boost .

Pada C ++ 20, konsep menjadi fitur resmi bahasa.

macbirdie
sumber
2
Yah, itu adalah mungkin, tapi konsep pemeriksaan masih merupakan ide yang baik. :)
j_random_hacker
Sebenarnya saya maksudkan bahwa itu tidak mungkin di "biasa" C ++. ;)
macbirdie
5
class Base
{
    struct FooSecurity{};
};

template<class Type>
class Foo
{
    typename Type::FooSecurity If_You_Are_Reading_This_You_Tried_To_Create_An_Instance_Of_Foo_For_An_Invalid_Type;
};

Pastikan kelas turunan mewarisi struktur FooSecurity dan kompiler akan marah di semua tempat yang tepat.

Stuart
sumber
@Zehelvion Type::FooSecurity digunakan di kelas templat. Jika kelas, yang diteruskan dalam argumen templat, belum FooSecurity, mencoba menggunakannya menyebabkan kesalahan. Ini yakin bahwa jika kelas yang lulus dalam argumen templat tidak memiliki FooSecurity, maka tidak berasal dari Base.
GingerPlusPlus
2

Penggunaan konsep C ++ 20

https://en.cppreference.com/w/cpp/language/constraints cppreference memberikan case use pewarisan sebagai contoh konsep eksplisit:

template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Base> T>
void f(T);  // T is constrained by Derived<T, Base>

Untuk beberapa basis, saya menduga sintaksnya adalah:

template <class T, class U, class V>
concept Derived = std::is_base_of<U, T>::value || std::is_base_of<V, T>::value;
 
template<Derived<Base1, Base2> T>
void f(T);

GCC 10 tampaknya telah menerapkannya: https://gcc.gnu.org/gcc-10/changes.html dan Anda bisa mendapatkannya sebagai PPA di Ubuntu 20.04 . https://godbolt.org/ GCC 10.1 lokal saya belum mengenali concept, jadi tidak yakin apa yang sedang terjadi.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
1

Apakah ada yang setara dengan kata kunci ini di C ++?

Tidak.

Bergantung pada apa yang ingin Anda capai, mungkin ada pengganti yang memadai (atau bahkan lebih baik).

Saya telah melihat melalui beberapa kode STL (di linux, saya pikir itu yang berasal dari implementasi SGI). Ini memiliki "pernyataan konsep"; misalnya, jika Anda memerlukan tipe yang mengerti *xdan ++x, pernyataan konsep akan berisi kode itu dalam fungsi do-nothing (atau yang serupa). Memang membutuhkan beberapa overhead, jadi mungkin pintar untuk memasukkannya ke dalam makro yang tergantung definisi #ifdef debug.

Jika hubungan subclass benar-benar ingin Anda ketahui, Anda dapat menegaskan dalam konstruktor bahwa T instanceof list(kecuali itu "dieja" secara berbeda dalam C ++). Dengan begitu, Anda dapat menguji jalan keluar dari kompiler karena tidak dapat memeriksanya.

Jonas Kölker
sumber
1

Tidak ada kata kunci untuk pemeriksaan jenis tersebut, tetapi Anda dapat memasukkan beberapa kode yang setidaknya akan gagal secara berurutan:

(1) Jika Anda ingin templat fungsi hanya menerima parameter dari kelas dasar X tertentu, tetapkan ke referensi X di fungsi Anda. (2) Jika Anda ingin menerima fungsi tetapi bukan primitif atau sebaliknya, atau Anda ingin memfilter kelas dengan cara lain, panggil fungsi pembantu template (kosong) di dalam fungsi Anda yang hanya ditentukan untuk kelas yang ingin Anda terima.

Anda dapat menggunakan (1) dan (2) juga dalam fungsi anggota kelas untuk memaksa pemeriksaan jenis ini di seluruh kelas.

Anda mungkin bisa memasukkannya ke Makro pintar untuk mengurangi rasa sakit Anda. :)

Jaap
sumber
-2

Nah, Anda bisa membuat template Anda membaca sesuatu seperti ini:

template<typename T>
class ObservableList {
  std::list<T> contained_data;
};

Namun ini akan membuat pembatasan tersirat, ditambah Anda tidak bisa hanya menyediakan apa pun yang tampak seperti daftar. Ada cara lain untuk membatasi jenis wadah yang digunakan, misalnya dengan memanfaatkan jenis iterator spesifik yang tidak ada di semua wadah tetapi sekali lagi ini lebih merupakan implisit daripada pembatasan eksplisit.

Sepengetahuan saya, sebuah konstruksi yang akan mencerminkan pernyataan Java pernyataan untuk sepenuhnya tidak ada dalam standar saat ini.

Ada cara untuk membatasi jenis yang dapat Anda gunakan di dalam template yang Anda tulis dengan menggunakan typedef tertentu di dalam template Anda. Ini akan memastikan bahwa kompilasi spesialisasi templat untuk jenis yang tidak termasuk typedef tertentu akan gagal, sehingga Anda dapat mendukung / tidak mendukung jenis tertentu secara selektif.

Dalam C ++ 11, pengenalan konsep harus membuat ini lebih mudah, tetapi saya tidak berpikir itu akan melakukan apa yang Anda inginkan.

Timo Geusch
sumber