C ++ typedef sangat diketik

50

Saya sudah mencoba memikirkan cara mendeklarasikan typedef yang sangat diketik, untuk menangkap kelas bug tertentu pada tahap kompilasi. Seringkali saya mengetikkan int menjadi beberapa jenis id, atau vektor untuk posisi atau kecepatan:

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

Ini bisa membuat maksud kode lebih jelas, tetapi setelah semalaman pengkodean, orang mungkin membuat kesalahan konyol seperti membandingkan berbagai jenis id, atau menambahkan posisi ke kecepatan mungkin.

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

Sayangnya, saran yang saya temukan untuk typedef yang sangat diketik termasuk menggunakan boost, yang setidaknya bagi saya bukan kemungkinan (saya punya setidaknya c ++ 11). Jadi setelah berpikir sebentar, saya menemukan ide ini, dan ingin menjalankannya oleh seseorang.

Pertama, Anda mendeklarasikan tipe dasar sebagai templat. Parameter template tidak digunakan untuk apa pun dalam definisi, namun:

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

Fungsi teman sebenarnya harus dideklarasikan ke depan sebelum definisi kelas, yang memerlukan deklarasi maju dari kelas template.

Kami kemudian mendefinisikan semua anggota untuk tipe dasar, hanya mengingat bahwa itu adalah kelas templat.

Akhirnya, ketika kita ingin menggunakannya, kita mengetikkannya sebagai:

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

Jenis sekarang sepenuhnya terpisah. Fungsi yang mengambil EntityID akan menimbulkan kesalahan kompiler jika Anda mencoba untuk memberi mereka ModelID, misalnya. Selain harus mendeklarasikan tipe dasar sebagai templat, dengan masalah yang menyertainya, itu juga cukup kompak.

Saya berharap ada yang punya komentar atau kritik tentang ide ini?

Salah satu masalah yang terlintas dalam pikiran ketika menulis ini, dalam hal posisi dan kecepatan misalnya, adalah bahwa saya tidak dapat mengkonversi antar tipe dengan bebas seperti sebelumnya. Di mana sebelum mengalikan vektor dengan skalar akan memberikan vektor lain, jadi saya bisa melakukan:

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;

Dengan typedef saya yang sangat diketik saya harus memberi tahu kompiler bahwa multypling Velocity by a Time menghasilkan Posisi.

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

Untuk mengatasi ini, saya pikir saya harus mengkhususkan setiap konversi secara eksplisit, yang dapat menjadi masalah. Di sisi lain, batasan ini dapat membantu mencegah jenis kesalahan lainnya (katakanlah, mengalikan Kecepatan dengan Jarak, mungkin, yang tidak masuk akal dalam domain ini). Jadi saya bingung, dan bertanya-tanya apakah orang punya pendapat tentang masalah asli saya, atau pendekatan saya untuk menyelesaikannya.

Kian
sumber
Lihatlah ini: zumalifeguard.wikia.com/wiki/Idtypes.idl
zumalifeguard
pertanyaan yang sama ada di sini: stackoverflow.com/q/23726038/476681
BЈовић

Jawaban:

40

Ini adalah parameter tipe hantu , yaitu, parameter tipe parameter yang digunakan bukan untuk representasi mereka, tetapi untuk memisahkan “spasi” jenis yang berbeda dengan representasi yang sama.

Dan berbicara tentang spasi, itu aplikasi yang berguna dari jenis hantu:

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) {  }

Seperti yang Anda lihat, ada beberapa kesulitan dengan tipe unit. Satu hal yang dapat Anda lakukan adalah menguraikan unit menjadi vektor eksponen bilangan bulat pada komponen dasar:

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;

Di sini kita menggunakan nilai phantom untuk menandai nilai runtime dengan informasi waktu kompilasi tentang eksponen pada unit yang terlibat. Ini berskala lebih baik daripada membuat struktur terpisah untuk kecepatan, jarak, dan sebagainya, dan mungkin cukup untuk menutupi kasus penggunaan Anda.

Jon Purdy
sumber
2
Hmm, menggunakan sistem template untuk menegakkan unit pada operasi itu keren. Tidak memikirkannya, terima kasih! Sekarang saya ingin tahu apakah Anda dapat menerapkan hal-hal seperti konversi antara meter dan kilometer, misalnya.
Kian
@Kian: Mungkin Anda akan menggunakan unit dasar SI secara internal — m, kg, s, A, & c. — dan hanya mendefinisikan alias 1km = 1000m untuk kenyamanan.
Jon Purdy
7

Saya memiliki kasus serupa di mana saya ingin membedakan makna yang berbeda dari beberapa nilai integer, dan melarang konversi implisit di antara mereka. Saya menulis kelas generik seperti ini:

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

Tentu saja jika Anda ingin lebih aman, Anda dapat membuat Tkonstruktornya explicitjuga. Ini Meaningkemudian digunakan seperti ini:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
mindriot
sumber
1
Ini menarik, tapi saya tidak yakin itu cukup kuat. Ini akan memastikan bahwa jika saya mendeklarasikan fungsi dengan tipe typedefed, hanya elemen yang tepat yang dapat digunakan sebagai parameter, yang bagus. Tetapi untuk setiap penggunaan lainnya menambahkan overhead sintaksis tanpa mencegah pencampuran parameter. Katakanlah operasi seperti membandingkan. operator == (int, int) akan mengambil EntityID dan ModelID tanpa keluhan (bahkan jika secara eksplisit mengharuskan saya melemparkannya, itu tidak membuat saya tidak menggunakan variabel yang salah).
Kian
Iya. Dalam kasus saya, saya harus mencegah diri saya menetapkan berbagai jenis ID satu sama lain. Perbandingan dan operasi aritmatika bukan perhatian utama saya. Konstruk di atas akan melarang penugasan, tetapi tidak untuk operasi lainnya.
mindriot
Jika Anda ingin menambah energi, Anda dapat membangun versi generik (yang wajar) yang menangani operator juga, dengan membuat kelas Eksplisit membungkus operator yang paling umum. Lihat pastebin.com/FQDuAXdu untuk contoh - Anda memerlukan beberapa konstruksi SFINAE yang cukup kompleks untuk menentukan apakah kelas wrapper benar-benar menyediakan operator yang dibungkus atau tidak (lihat pertanyaan SO ini ). Pikiran Anda, itu masih tidak dapat mencakup semua kasus dan mungkin tidak sepadan dengan masalahnya.
mindriot
Meskipun secara sintaksis elegan, solusi ini akan dikenakan penalti kinerja yang signifikan untuk tipe integer. Bilangan bulat dapat dilewatkan melalui register, struct (bahkan mengandung satu integer) tidak bisa.
Ghostrider
1

Saya tidak yakin bagaimana cara berikut ini bekerja dalam kode produksi (saya seorang pemula C ++ / pemrograman, seperti, pemula CS101), tapi saya memasak ini menggunakan sistem makro 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
sumber
Catatan: Tolong beri tahu saya jika ada masalah / perbaikan yang Anda pikirkan.
Noein
1
Bisakah Anda menambahkan beberapa kode yang menunjukkan bagaimana makro ini digunakan - seperti pada contoh di pertanyaan asli? Jika demikian, ini adalah jawaban yang bagus.
Jay Elston