C ++ พิมพ์ typedef อย่างยิ่ง


49

ฉันพยายามคิดวิธีประกาศ typedefs พิมพ์อย่างยิ่งเพื่อจับระดับหนึ่งของข้อบกพร่องในขั้นตอนการรวบรวม มันมักจะเป็นกรณีที่ฉันจะพิมพ์ int ลงในรหัสหลายประเภทหรือเวกเตอร์ไปยังตำแหน่งหรือความเร็ว:

typedef int EntityID;
typedef int ModelID;
typedef Vector3 Position;
typedef Vector3 Velocity;

สิ่งนี้สามารถทำให้เจตนาของรหัสชัดเจนยิ่งขึ้น แต่หลังจากการเขียนโค้ดคืนหนึ่งอาจทำให้เกิดข้อผิดพลาดที่โง่เขลาเช่นการเปรียบเทียบรหัสต่าง ๆ หรือเพิ่มตำแหน่งให้กับความเร็ว

EntityID eID;
ModelID mID;

if ( eID == mID ) // <- Compiler sees nothing wrong
{ /*bug*/ }


Position p;
Velocity v;

Position newP = p + v; // bug, meant p + v*s but compiler sees nothing wrong

น่าเสียดายที่คำแนะนำที่ฉันพบสำหรับ typedefs ที่พิมพ์อย่างมากรวมถึงการใช้ boost ซึ่งอย่างน้อยสำหรับฉันก็เป็นไปไม่ได้ (ฉันมี c ++ 11 อย่างน้อย) ดังนั้นหลังจากความคิดเล็กน้อยฉันก็มาถึงความคิดนี้และต้องการที่จะดำเนินการโดยใครบางคน

ก่อนอื่นคุณต้องประกาศประเภทฐานเป็นแม่แบบ พารามิเตอร์เทมเพลตไม่ได้ใช้สำหรับสิ่งใด ๆ ในนิยามอย่างไรก็ตาม:

template < typename T >
class IDType
{
    unsigned int m_id;

    public:
        IDType( unsigned int const& i_id ): m_id {i_id} {};
        friend bool operator==<T>( IDType<T> const& i_lhs, IDType<T> const& i_rhs );
};

ฟังก์ชั่นเพื่อนจะต้องมีการประกาศไปข้างหน้าก่อนการกำหนดคลาสซึ่งต้องมีการประกาศล่วงหน้าของเทมเพลตคลาส

จากนั้นเราจะกำหนดสมาชิกทั้งหมดสำหรับประเภทฐานเพียงแค่จำได้ว่ามันเป็นคลาสแม่แบบ

ในที่สุดเมื่อเราต้องการใช้งานเราพิมพ์มันเป็น:

class EntityT;
typedef IDType<EntityT> EntityID;
class ModelT;
typedef IDType<ModelT> ModelID;

ประเภทนี้แยกจากกันโดยสิ้นเชิง ฟังก์ชั่นที่ใช้ EntityID จะทำให้เกิดข้อผิดพลาดของคอมไพเลอร์หากคุณพยายามที่จะป้อน ModelID ให้พวกเขาแทน นอกเหนือจากการประกาศประเภทพื้นฐานเป็นแม่แบบด้วยปัญหาที่เกี่ยวข้องก็ยังมีขนาดกะทัดรัด

ฉันหวังว่าทุกคนจะมีความคิดเห็นหรือวิจารณ์เกี่ยวกับความคิดนี้

ประเด็นหนึ่งที่นึกขึ้นมาได้ในขณะที่เขียนสิ่งนี้ในกรณีของตำแหน่งและความเร็วเช่นว่าฉันจะไม่สามารถแปลงระหว่างประเภทต่าง ๆ ได้อย่างอิสระเหมือนเมื่อก่อน ที่ไหนก่อนที่จะคูณเวกเตอร์ด้วยสเกลาร์จะให้เวกเตอร์อีกอัน, ดังนั้นฉันทำได้:

typedef float Time;
typedef Vector3 Position;
typedef Vector3 Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t;

ด้วย typedef ที่พิมพ์ออกมาอย่างแรงของฉันฉันต้องบอกคอมไพเลอร์ว่าการคูณความเร็วด้วยเวลาทำให้ผลลัพธ์อยู่ในตำแหน่ง

class TimeT;
typedef Float<TimeT> Time;
class PositionT;
typedef Vector3<PositionT> Position;
class VelocityT;
typedef Vector3<VelocityT> Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t; // Compiler error

ในการแก้ปัญหานี้ฉันคิดว่าฉันต้องชำนาญทุกการแปลงอย่างชัดเจนซึ่งอาจเป็นเรื่องที่น่ารำคาญ ในทางกลับกันข้อ จำกัด นี้สามารถช่วยป้องกันข้อผิดพลาดประเภทอื่น ๆ (เช่นการคูณความเร็วด้วยระยะทางบางทีอาจไม่สมเหตุสมผลในโดเมนนี้) ดังนั้นฉันจึงถูกฉีกขาดและสงสัยว่าผู้คนมีความคิดเห็นใด ๆ เกี่ยวกับปัญหาดั้งเดิมของฉันหรือวิธีการของฉันในการแก้ไข



คำถามเดียวกันอยู่ที่นี่: stackoverflow.com/q/23726038/476681
BЈовић

คำตอบ:


39

เหล่านี้คือพารามิเตอร์ประเภทแฟนทอมนั่นคือพารามิเตอร์ของประเภทพารามิเตอร์ที่ใช้ไม่ได้สำหรับการเป็นตัวแทนของพวกเขา แต่เพื่อแยก "ช่องว่าง" ประเภทที่แตกต่างด้วยการเป็นตัวแทนเดียวกัน

และเมื่อพูดถึงช่องว่างนั่นเป็นแอพพลิเคชั่นที่มีประโยชน์สำหรับประเภท phantom:

template<typename Space>
struct Point { double x, y; };

struct WorldSpace;
struct ScreenSpace;

// Conversions between coordinate spaces are explicit.
Point<ScreenSpace> project(Point<WorldSpace> p, const Camera& c) {  }

อย่างที่คุณเห็นมีปัญหาบางอย่างกับหน่วยประเภท สิ่งหนึ่งที่คุณสามารถทำได้คือการสลายหน่วยเป็นเวกเตอร์ของเลขชี้กำลังจำนวนเต็มบนส่วนประกอบพื้นฐาน:

template<typename T, int Meters, int Seconds>
struct Unit {
  Unit(const T& value) : value(value) {}
  T value;
};

template<typename T, int MA, int MB, int SA, int SB>
Unit<T, MA - MB, SA - SB>
operator/(const Unit<T, MA, SA>& a, const Unit<T, MB, SB>& b) {
  return a.value / b.value;
}

Unit<double, 0, 0> one(1);
Unit<double, 1, 0> one_meter(1);
Unit<double, 0, 1> one_second(1);

// Unit<double, 1, -1>
auto one_meter_per_second = one_meter / one_second;

ที่นี่เราใช้ค่าแฟนทอมเพื่อแท็กค่ารันไทม์พร้อมข้อมูลเวลาคอมไพล์เกี่ยวกับเลขชี้กำลังในหน่วยที่เกี่ยวข้อง เครื่องชั่งนี้ดีกว่าการสร้างโครงสร้างแยกต่างหากสำหรับความเร็วระยะทางและอื่น ๆ และอาจเพียงพอที่จะครอบคลุมกรณีการใช้งานของคุณ


2
อืมการใช้ระบบเทมเพลตสำหรับการบังคับใช้หน่วยในการปฏิบัติงานนั้นยอดเยี่ยม ไม่เคยคิดเลยขอบคุณ! ตอนนี้ฉันสงสัยว่าคุณสามารถบังคับใช้สิ่งต่าง ๆ เช่นการแปลงระหว่างเมตรและกิโลเมตรหรือไม่
Kian

@Kian: สมมุติว่าคุณจะใช้หน่วยฐาน SI ภายใน - m, kg, s, A, & c - และเพียงแค่กำหนดนามแฝง 1km = 1000m เพื่อความสะดวก
Jon Purdy

7

ฉันมีกรณีที่คล้ายกันซึ่งฉันต้องการแยกแยะความหมายที่แตกต่างกันของค่าจำนวนเต็มบางอย่างและห้ามการแปลงโดยนัยระหว่างพวกเขา ฉันเขียนชั้นเรียนทั่วไปเช่นนี้:

template <typename T, typename Meaning>
struct Explicit
{
  //! Default constructor does not initialize the value.
  Explicit()
  { }

  //! Construction from a fundamental value.
  Explicit(T value)
    : value(value)
  { }

  //! Implicit conversion back to the fundamental data type.
  inline operator T () const { return value; }

  //! The actual fundamental value.
  T value;
};

แน่นอนถ้าคุณต้องการที่จะปลอดภัยมากขึ้นคุณสามารถสร้างคอนTสตรัคได้explicitเช่นกัน Meaningที่ใช้แล้วเช่นนี้

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;

1
สิ่งนี้น่าสนใจ แต่ฉันไม่แน่ใจว่ามันค่อนข้างแข็งแกร่งพอ มันจะช่วยให้มั่นใจได้ว่าถ้าฉันประกาศฟังก์ชั่นที่มีประเภทที่พิมพ์ไว้เฉพาะองค์ประกอบที่เหมาะสมสามารถใช้เป็นพารามิเตอร์ซึ่งเป็นสิ่งที่ดี แต่สำหรับทุกการใช้งานอื่น ๆ มันจะเพิ่มค่าโสหุ้ย syntactic โดยไม่ป้องกันการผสมของพารามิเตอร์ พูดการดำเนินงานเช่นการเปรียบเทียบ โอเปอเรเตอร์ == (int, int) จะใช้ EntityID และ ModelID โดยไม่มีการร้องเรียน (แม้ว่าจะมีความต้องการอย่างชัดเจนว่าฉันต้องใช้งานมันก็ไม่ได้ป้องกันไม่ให้ฉันใช้ตัวแปรผิด)
Kian

ใช่. ในกรณีของฉันฉันต้องป้องกันตัวเองจากการกำหนดรหัสต่าง ๆ ให้กัน การเปรียบเทียบและการดำเนินการทางคณิตศาสตร์ไม่ใช่ข้อกังวลหลักของฉัน โครงสร้างข้างต้นจะห้ามการมอบหมาย แต่ไม่ใช่การดำเนินการอื่น
mindriot

หากคุณยินดีที่จะเพิ่มพลังงานให้กับสิ่งนี้คุณสามารถสร้างเวอร์ชันทั่วไป (พอใช้) ที่จัดการกับตัวดำเนินการได้เช่นกันโดยการทำให้คลาส Explicit ล้อมรอบตัวดำเนินการทั่วไป ดูpastebin.com/FQDuAXduสำหรับตัวอย่าง - คุณต้องการโครงสร้างที่ซับซ้อนพอสมควรของ SFINAE เพื่อตรวจสอบว่าคลาส wrapper จัดเตรียมโอเปอเรเตอร์ที่ห่อหุ้มหรือไม่ (ดูคำถาม SO นี้ ) โปรดทราบว่ามันยังไม่ครอบคลุมทุกกรณีและอาจไม่คุ้มค่ากับปัญหา
mindriot

ในขณะที่วากยสัมพันธ์ที่งดงามโซลูชันนี้จะต้องเสียค่าปรับที่สำคัญสำหรับประเภทจำนวนเต็ม จำนวนเต็มสามารถส่งผ่านลงทะเบียน structs (แม้จะมีจำนวนเต็มเดียว) ไม่สามารถ
Ghostrider

1

ฉันไม่แน่ใจว่าวิธีการทำงานต่อไปนี้ในรหัสการผลิต (ฉันเป็น C ++ / การเขียนโปรแกรมเริ่มต้นเช่น CS101 เริ่มต้น) แต่ฉันทำมันขึ้นมาโดยใช้แมโครของ C ++

#define newtype(type_, type_alias) struct type_alias { \

/* make a new struct type with one value field
of a specified type (could be another struct with appropriate `=` operator*/

    type_ inner_public_field_thing; \  // the masked_value
    \
    explicit type_alias( type_ new_value ) { \  // the casting through a constructor
    // not sure how this'll work when casting non-const values
    // (like `type_alias(variable)` as opposed to `type_alias(bare_value)`
        inner_public_field_thing = new_value; } }

หมายเหตุ: โปรดแจ้งให้เราทราบถึงข้อผิดพลาด / การปรับปรุงที่คุณคิด
Noein

1
คุณสามารถเพิ่มรหัสที่แสดงวิธีใช้มาโครนี้ - เช่นในตัวอย่างในคำถามเดิมได้หรือไม่ ถ้าเป็นเช่นนั้นนี่เป็นคำตอบที่ดี
Jay Elston
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.