ใน Java คุณสามารถกำหนดคลาสทั่วไปที่ยอมรับเฉพาะประเภทที่ขยายคลาสที่คุณเลือกเช่น:
public class ObservableList<T extends List> {
...
}
ทำได้โดยใช้คำหลัก "ขยาย"
มีบางอย่างที่เทียบเท่ากับคำหลักนี้ใน C ++?
ใน Java คุณสามารถกำหนดคลาสทั่วไปที่ยอมรับเฉพาะประเภทที่ขยายคลาสที่คุณเลือกเช่น:
public class ObservableList<T extends List> {
...
}
ทำได้โดยใช้คำหลัก "ขยาย"
มีบางอย่างที่เทียบเท่ากับคำหลักนี้ใน C ++?
คำตอบ:
ฉันขอแนะนำให้ใช้คุณลักษณะยืนยันแบบคงที่ของ Boost ในคอนเสิร์ตด้วยis_base_of
จากไลบรารี 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
...
};
ในบางกรณีที่ง่ายกว่านั้นคุณสามารถส่งต่อแม่แบบส่วนกลางได้ แต่จะกำหนดเฉพาะ (เฉพาะบางส่วนหรืออย่างชัดเจน) สำหรับประเภทที่ถูกต้อง:
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.
[ไมเนอร์ EDIT 6/12/2556: การใช้เทมเพลตที่ประกาศ แต่ไม่ได้กำหนดจะส่งผลให้ตัวเชื่อมโยงไม่ใช่คอมไพเลอร์และข้อความแสดงข้อผิดพลาด]
myBaseType
เผง ก่อนที่จะปิดการใช้งาน Boost คุณควรทราบว่าส่วนใหญ่เป็นรหัสเทมเพลตสำหรับส่วนหัวเท่านั้นดังนั้นจึงไม่มีหน่วยความจำหรือเวลาที่ต้องใช้งานจริงสำหรับสิ่งที่คุณไม่ได้ใช้ นอกจากนี้สิ่งต่าง ๆ ที่คุณจะใช้ที่นี่ ( BOOST_STATIC_ASSERT()
และis_base_of<>
) สามารถใช้งานได้โดยการประกาศเท่านั้น(เช่นไม่มีคำจำกัดความที่แท้จริงของฟังก์ชันหรือตัวแปร) ดังนั้นพวกเขาจะไม่ใช้พื้นที่หรือเวลาใด ๆ
static_assert(std::is_base_of<List, T>::value, "T must extend list")
ตอนนี้เราสามารถใช้
my_template<int> x;
หรือmy_template<float**> y;
ตรวจสอบว่าคอมไพเลอร์อนุญาตเหล่านี้แล้วประกาศตัวแปรmy_template<char> z;
และตรวจสอบว่าไม่
ซึ่งโดยทั่วไปจะไม่ได้รับการรับรองใน C ++ เนื่องจากมีคำตอบอื่น ๆ ที่ระบุไว้ที่นี่ ใน C ++ เรามักจะนิยามประเภททั่วไปตามข้อ จำกัด อื่นนอกเหนือจาก "การสืบทอดจากคลาสนี้" หากคุณต้องการทำสิ่งนั้นจริง ๆ มันค่อนข้างง่ายที่จะทำใน C ++ 11 และ<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..
};
สิ่งนี้ทำลายแนวคิดจำนวนมากที่ผู้คนคาดหวังใน C ++ มันเป็นการดีกว่าที่จะใช้ลูกเล่นเช่นการกำหนดลักษณะของคุณ ยกตัวอย่างเช่นอาจจะobservable_list
ต้องการที่จะยอมรับทุกประเภทของภาชนะที่มี typedefs const_iterator
และbegin
และฟังก์ชั่นของสมาชิกที่ผลตอบแทนend
const_iterator
หากคุณ จำกัด สิ่งนี้ไว้ในคลาสที่สืบทอดจากlist
นั้นผู้ใช้ที่มีประเภทของตัวเองที่ไม่ได้รับมรดกlist
แต่ให้ฟังก์ชั่นสมาชิกและ typedefs เหล่านี้จะไม่สามารถใช้งานของคุณobservable_list
ได้
มีสองวิธีแก้ไขปัญหานี้หนึ่งในนั้นคือการไม่ จำกัด อะไรและพึ่งพาการพิมพ์เป็ด ข้อผิดพลาดใหญ่ในการแก้ปัญหานี้คือมันเกี่ยวข้องกับข้อผิดพลาดจำนวนมากที่ผู้ใช้อาจหายาก อีกวิธีหนึ่งคือการกำหนดลักษณะเพื่อ จำกัด ประเภทที่มีให้เพื่อตอบสนองความต้องการของอินเทอร์เฟซ ข้อเสียใหญ่สำหรับโซลูชันนี้คือการเขียนเพิ่มเติมซึ่งสามารถดูน่ารำคาญ อย่างไรก็ตามด้านบวกคือคุณจะสามารถเขียนข้อความแสดงข้อผิดพลาดของคุณเองได้static_assert
ได้
เพื่อความสมบูรณ์การแก้ปัญหาของตัวอย่างด้านบนได้รับ:
#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...
};
มีแนวคิดมากมายที่แสดงในตัวอย่างด้านบนที่แสดงคุณลักษณะของ C ++ 11 ข้อความค้นหาบางคำสำหรับคนที่อยากรู้อยากเห็นคือเทมเพลต Variadic, SFINAE, นิพจน์ SFINAE และลักษณะของประเภท
template<class T:list>
แนวคิดดังกล่าวจึงเป็นความผิด ขอบคุณสำหรับทิป.
ทางออกที่ง่ายซึ่งยังไม่มีใครพูดถึงคือการเพิกเฉยต่อปัญหา ถ้าฉันลองใช้int
เป็นเทมเพลตประเภทในเทมเพลตฟังก์ชั่นที่ต้องการคลาสคอนเทนเนอร์เช่นเวกเตอร์หรือรายการฉันจะได้รับข้อผิดพลาดในการคอมไพล์ เรียบง่าย แต่ก็แก้ปัญหาได้ คอมไพเลอร์จะพยายามใช้ประเภทที่คุณระบุและหากล้มเหลวระบบจะสร้างข้อผิดพลาดในการคอมไพล์
ปัญหาเดียวกับที่เป็นข้อความผิดพลาดที่คุณได้รับจะเป็นเรื่องยุ่งยากในการอ่าน มันเป็นวิธีที่ใช้กันทั่วไปในการทำสิ่งนี้ ไลบรารีมาตรฐานเต็มไปด้วยฟังก์ชั่นหรือแม่แบบคลาสที่คาดหวังพฤติกรรมบางอย่างจากประเภทแม่แบบและไม่ต้องทำอะไรเพื่อตรวจสอบว่าประเภทที่ใช้นั้นถูกต้อง
หากคุณต้องการข้อความแสดงข้อผิดพลาดที่ดีกว่า (หรือถ้าคุณต้องการที่จะตรวจจับกรณีที่จะไม่เกิดข้อผิดพลาดของคอมไพเลอร์ แต่ก็ยังไม่สมเหตุสมผล) คุณสามารถทำได้โดยขึ้นอยู่กับความซับซ้อนที่คุณต้องการ ไลบรารี Boost concept_check
ด้วยคอมไพเลอร์ล่าสุดคุณมี built_in static_assert
ซึ่งสามารถใช้แทนได้
T
และรหัสนี้เรียกว่าที่ไหน หากไม่มีบริบทฉันก็ไม่มีโอกาสเข้าใจข้อมูลโค้ดนั้น แต่สิ่งที่ฉันพูดนั้นเป็นความจริง หากคุณพยายามโทรหาtoString()
ประเภทที่ไม่มีtoString
ฟังก์ชันสมาชิกคุณจะได้รับข้อผิดพลาดในการคอมไพล์
เราสามารถใช้std::is_base_of
และstd::enable_if
:
( static_assert
สามารถลบออกได้คลาสด้านบนสามารถปรับใช้เองหรือใช้จากการเพิ่มหากเราไม่สามารถอ้างอิงได้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
}
เท่าที่ฉันรู้ว่าสิ่งนี้เป็นไปไม่ได้ใน C ++ อย่างไรก็ตามมีแผนจะเพิ่มคุณสมบัติที่เรียกว่า "แนวคิด" ในมาตรฐาน C ++ 0x ใหม่ที่ให้ฟังก์ชันการทำงานที่คุณต้องการ นี้บทความวิกิพีเดียเกี่ยวกับ C ++ แนวคิดจะอธิบายในรายละเอียดมากขึ้น
ฉันรู้ว่านี่ไม่ได้แก้ปัญหาของคุณทันที แต่มีคอมไพเลอร์ C ++ บางตัวที่เริ่มเพิ่มคุณสมบัติจากมาตรฐานใหม่แล้วดังนั้นจึงอาจเป็นไปได้ที่จะหาคอมไพเลอร์ที่ใช้คุณสมบัติแนวคิดนี้ไปแล้ว
static_assert
และ SFINAE ดังที่คำตอบอื่น ๆ แสดง ปัญหาที่เหลือสำหรับใครบางคนที่มาจาก Java หรือ C # หรือ Haskell (... ) คือคอมไพเลอร์ C ++ 20 ไม่ได้ทำการตรวจสอบคำนิยามกับแนวคิดที่จำเป็นซึ่ง Java และ C # ทำ
ฉันคิดว่าคำตอบก่อนหน้านี้หมดไปจากการมองเห็นป่าต้นไม้
generics Java ไม่ได้เช่นเดียวกับแม่ ; พวกเขาใช้ลบออกประเภทซึ่งเป็นเทคนิคแบบไดนามิกมากกว่าpolymorphism รวบรวมเวลาซึ่งเป็นเทคนิคแบบคงที่ มันควรจะชัดเจนว่าทำไมทั้งสองกลยุทธ์ที่แตกต่างกันมากจึงไม่เจลได้ดี
แทนที่จะพยายามใช้โครงสร้างเวลาคอมไพล์เพื่อจำลองเวลารันหนึ่งลองดูสิ่งที่ทำextends
จริง: ตาม Stack OverflowและWikipedia การขยายใช้เพื่อระบุ subclassing
C ++ ยังรองรับ subclassing
นอกจากนี้คุณยังแสดงคลาสคอนเทนเนอร์ซึ่งใช้การลบประเภทในรูปแบบทั่วไปและขยายเพื่อดำเนินการตรวจสอบชนิด ใน C ++ คุณต้องทำเครื่องจักรสำหรับลบประเภทด้วยตัวเองซึ่งง่าย: สร้างตัวชี้ไปที่ซูเปอร์คลาส
ลองห่อมันลงใน typedef เพื่อให้ใช้งานง่ายกว่าทำทั้งคลาสและ voila:
typedef std::list<superclass*> subclasses_of_superclass_only_list;
ตัวอย่างเช่น:
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
ตอนนี้ดูเหมือนว่า List เป็นอินเทอร์เฟซแทนการเรียงลำดับของคอลเลกชัน อินเทอร์เฟซใน C ++ จะเป็นเพียงคลาสนามธรรมนั่นคือคลาสที่ไม่ใช้อะไรนอกจากวิธีเสมือนจริงล้วนๆ เมื่อใช้วิธีนี้คุณสามารถใช้ตัวอย่าง Java ของคุณใน C ++ ได้อย่างง่ายดายโดยไม่ต้องมีแนวคิดหรือแม่แบบเฉพาะทาง นอกจากนี้มันยังทำงานได้ช้าเท่ากับจาวาสไตล์ทั่วไปเนื่องจากการค้นหาตารางเสมือน แต่สิ่งนี้อาจเป็นการสูญเสียที่ยอมรับได้
เทียบเท่าที่ยอมรับเฉพาะประเภท T ที่ได้มาจากรายการประเภทดูเหมือน
template<typename T,
typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
// ...
};
บทสรุปผู้บริหาร: อย่าทำอย่างนั้น
คำตอบ j_random_hacker บอกวิธีการทำเช่นนี้ อย่างไรก็ตามฉันอยากจะชี้ให้เห็นว่าคุณไม่ควรทำเช่นนี้ จุดรวมของเทมเพลตคือพวกเขาสามารถยอมรับชนิดที่เข้ากันได้ใด ๆ และข้อ จำกัด ชนิดของสไตล์ Java จะแตก
ข้อ จำกัด ประเภทของ Java เป็นข้อผิดพลาดไม่ใช่คุณสมบัติ พวกเขาอยู่ที่นั่นเพราะ Java จะลบประเภทใน generics ดังนั้น Java ไม่สามารถหาวิธีการเรียกวิธีการตามค่าพารามิเตอร์ประเภทเพียงอย่างเดียว
C ++ ในอีกทางหนึ่งไม่มีข้อ จำกัด ดังกล่าว ประเภทพารามิเตอร์เทมเพลตสามารถเป็นประเภทใดก็ได้ที่เข้ากันได้กับการดำเนินการที่ใช้ด้วย ไม่จำเป็นต้องเป็นคลาสพื้นฐานทั่วไป สิ่งนี้คล้ายกับ "Duck Typing" ของ Python แต่ทำในเวลารวบรวม
ตัวอย่างง่ายๆที่แสดงพลังของเทมเพลต:
// 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;
}
ฟังก์ชันผลรวมนี้สามารถรวมเวกเตอร์ชนิดใดก็ได้ที่สนับสนุนการดำเนินการที่ถูกต้อง มันทำงานร่วมกับทั้งแบบดั้งเดิมเช่น int / long / float / double และประเภทตัวเลขที่ผู้ใช้กำหนดเองซึ่งเกิน + + ผู้ประกอบการ เฮ้คุณสามารถใช้ฟังก์ชั่นนี้เพื่อเข้าร่วมสตริงได้เนื่องจากมันรองรับ + =
ไม่จำเป็นต้องใช้การชกมวย / การถอนการจั่ว
โปรดทราบว่ามันยังสร้างอินสแตนซ์ใหม่ของ T โดยใช้ T () สิ่งนี้เป็นเรื่องเล็กน้อยใน C ++ โดยใช้อินเตอร์เฟสแบบ implicit แต่เป็นไปไม่ได้ใน Java ที่มีข้อ จำกัด ประเภท
แม้ว่าเทมเพลต C ++ จะไม่มีข้อ จำกัด ชนิดชัดเจน แต่ก็ยังคงปลอดภัยและจะไม่คอมไพล์ด้วยรหัสที่ไม่สนับสนุนการดำเนินการที่ถูกต้อง
นั่นเป็นไปไม่ได้ใน C ++ ธรรมดา แต่คุณสามารถตรวจสอบพารามิเตอร์แม่แบบที่รวบรวมเวลาผ่านการตรวจสอบแนวคิดเช่นใช้BCCL ของ Boost
ตั้งแต่ C ++ 20 แนวคิดได้กลายเป็นคุณสมบัติที่เป็นทางการของภาษา
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;
};
ตรวจสอบให้แน่ใจว่าคลาสที่ได้รับสืบทอดมาโครงสร้าง FooSecurity และคอมไพเลอร์จะหงุดหงิดในที่ที่เหมาะสมทั้งหมด
Type::FooSecurity
ใช้ในคลาสเทมเพลต หากคลาสที่ส่งผ่านในอาร์กิวเมนต์แม่แบบไม่ได้FooSecurity
พยายามที่จะใช้มันทำให้เกิดข้อผิดพลาด มันเป็นที่แน่ใจว่าถ้าระดับผ่านในอาร์กิวเมนต์แม่แบบไม่ได้ FooSecurity Base
มันไม่ได้มาจาก
การใช้แนวคิด C ++ 20
https://en.cppreference.com/w/cpp/language/constraints cppreference ให้กรณีการใช้การสืบทอดเป็นตัวอย่างแนวคิดที่ชัดเจน:
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>
สำหรับหลายฐานฉันคาดเดาไวยากรณ์จะเป็น:
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 ดูเหมือนจะได้ดำเนินการมันhttps://gcc.gnu.org/gcc-10/changes.htmlและคุณจะได้รับมันเป็น PPA บน Ubuntu 20.04 https://godbolt.org/ GCC ท้องถิ่นของฉันยังไม่รู้จักconcept
ดังนั้นจึงไม่แน่ใจว่าเกิดอะไรขึ้น
มีบางอย่างที่เทียบเท่ากับคำหลักนี้ใน C ++?
เลขที่
ขึ้นอยู่กับสิ่งที่คุณพยายามที่จะบรรลุอาจมีทดแทนที่เพียงพอ (หรือดีกว่า)
ฉันดูรหัส STL (บน linux ฉันคิดว่าเป็นรหัสที่ได้มาจากการใช้งานของ SGI) มันมี "การยืนยันแนวคิด"; ตัวอย่างเช่นหากคุณต้องการประเภทที่เข้าใจ*x
และ++x
การยืนยันแนวคิดจะมีรหัสนั้นในฟังก์ชั่นไม่ทำอะไรเลย (หรือสิ่งที่คล้ายกัน) #ifdef debug
มันไม่จำเป็นต้องค่าใช้จ่ายบางอย่างเพื่อให้มันอาจจะเป็นสมาร์ทที่จะนำมันในแมโครที่มีความหมายขึ้นอยู่กับ
หากความสัมพันธ์คลาสย่อยเป็นสิ่งที่คุณต้องการทราบจริงๆคุณสามารถยืนยันใน Constructor ที่T instanceof list
(ยกเว้นว่า "สะกด" แตกต่างกันใน C ++) ด้วยวิธีนี้คุณสามารถทดสอบวิธีออกจากคอมไพเลอร์ที่ไม่สามารถตรวจสอบให้คุณได้
ไม่มีคำหลักสำหรับการตรวจสอบประเภทดังกล่าว แต่คุณสามารถใส่รหัสบางอย่างที่จะล้มเหลวอย่างเป็นระเบียบ:
(1) ถ้าคุณต้องการให้เท็มเพลตฟังก์ชั่นยอมรับเฉพาะพารามิเตอร์ของคลาสฐาน X ที่แน่นอนให้กำหนดให้กับการอ้างอิง X ในฟังก์ชันของคุณ (2) ถ้าคุณต้องการยอมรับฟังก์ชั่น แต่ไม่ใช่แบบดั้งเดิมหรือกลับกันหรือคุณต้องการกรองคลาสในวิธีอื่นให้เรียกใช้ฟังก์ชันตัวช่วยเทมเพลต (ว่าง) ภายในฟังก์ชันที่กำหนดไว้สำหรับคลาสที่คุณต้องการยอมรับเท่านั้น
คุณสามารถใช้ (1) และ (2) ในฟังก์ชันสมาชิกของคลาสเพื่อบังคับให้ตรวจสอบประเภทเหล่านี้ในคลาสทั้งหมด
คุณอาจจะใส่มันลงไปใน Macro ที่ชาญฉลาดเพื่อบรรเทาความเจ็บปวดของคุณ :)
คุณสามารถสร้างเทมเพลตของคุณอ่านดังนี้:
template<typename T>
class ObservableList {
std::list<T> contained_data;
};
อย่างไรก็ตามสิ่งนี้จะทำให้ข้อ จำกัด โดยนัยรวมถึงคุณไม่สามารถระบุสิ่งที่ดูเหมือนรายการได้ มีวิธีอื่น ๆ ในการ จำกัด ประเภทคอนเทนเนอร์ที่ใช้ตัวอย่างเช่นโดยการใช้ชนิดตัววนซ้ำเฉพาะที่ไม่ได้มีอยู่ในคอนเทนเนอร์ทั้งหมด แต่อีกครั้งนี่เป็นการบอกเป็นนัยมากกว่าข้อ จำกัด ที่ชัดเจน
ด้วยความรู้ที่ดีที่สุดของฉันสิ่งก่อสร้างที่จะสะท้อนถึงคำสั่ง Java ในขอบเขตที่สมบูรณ์ไม่มีอยู่ในมาตรฐานปัจจุบัน
มีวิธี จำกัด ประเภทที่คุณสามารถใช้ภายในแม่แบบที่คุณเขียนโดยใช้ typedefs เฉพาะภายในแม่แบบของคุณ สิ่งนี้จะช่วยให้มั่นใจได้ว่าการรวบรวมเทมเพลตเฉพาะทางสำหรับประเภทที่ไม่รวม typedef นั้นจะล้มเหลวดังนั้นคุณสามารถเลือกที่จะสนับสนุน / ไม่สนับสนุนบางประเภท
ใน C ++ 11 การแนะนำแนวคิดควรทำให้ง่ายขึ้น แต่ฉันไม่คิดว่ามันจะทำสิ่งที่คุณต้องการอย่างแน่นอน