Can My Code Mix Apples and Oranges?
In your C project, you use numerous "usings" to clarify the intended purpose of variables, primarily for std::string identifiers like PortalId or CakeId. Your current approach allows the following:
using PortalId = std::string; using CakeId = std::string; PortalId portal_id("2"); CakeId cake_id("is a lie"); portal_id = cake_id; // OK
But this flexibility is undesirable. You seek compile-time type checking to prevent mixing different object types while preserving the original object's functionality.
Can C Fulfill This Request?
Yes, C provides a solution for your dilemma. Using C 17 and some clever coding, it's possible to create a strongly typed identifier that prevents mixing types. The results look something like this:
SAFE_TYPEDEF(std::string, PortalId); SAFE_TYPEDEF(std::string, CakeId); int main() { PortalId portal_id("2"); CakeId cake_id("is a lie"); std::map<CakeId, PortalId> p_to_cake; // OK p_to_cake[cake_id] = portal_id; // OK p_to_cake[portal_id] = cake_id; // COMPILER ERROR portal_id = cake_id; // COMPILER ERROR portal_id = "1.0"; // COMPILER ERROR portal_id = PortalId("42"); // OK return 0; }
In this code, assignments between different types are not permitted, and adding incompatible types to containers will result in compiler errors.
What's the Secret?
The magic lies within the SAFE_TYPEDEF macro. Here's the updated version that handles all your requirements:
#include <iostream> #include <string> #include <map> #include <unordered_map> // define some tags to create uniqueness struct portal_tag {}; struct cake_tag {}; // a string-like identifier that is typed on a tag type template<class Tag> struct string_id { using tag_type = Tag; // needs to be default-constuctable because of use in map[] below string_id(std::string s) : _value(std::move(s)) {} string_id() : _value() {} // provide access to the underlying string value const std::string& value() const { return _value; } private: std::string _value; // will only compare against same type of id. friend bool operator < (const string_id& l, const string_id& r) { return l._value < r._value; } friend bool operator == (const string_id& l, const string_id& r) { return l._value == r._value; } // and let's go ahead and provide expected free functions friend auto to_string(const string_id& r) -> const std::string& { return r._value; } friend auto operator << (std::ostream& os, const string_id& sid) -> std::ostream& { return os << sid.value(); } friend std::size_t hash_code(const string_id& sid) { std::size_t seed = typeid(tag_type).hash_code(); seed ^= std::hash<std::string>()(sid._value); return seed; } }; // let's make it hashable namespace std { template<class Tag> struct hash<string_id<Tag>> { using argument_type = string_id<Tag>; using result_type = std::size_t; result_type operator()(const argument_type& arg) const { return hash_code(arg); } }; } // create some type aliases for ease of use using PortalId = string_id<portal_tag>; using CakeId = string_id<cake_tag>; using namespace std;
This code defines a string_id class that serves as a strongly typed identifier. It's tagged with either portal_tag or cake_tag to ensure uniqueness. The class also overloads operators like <, "==", and free functions such as to_string, <<, and hash_code.
With this implementation, you can enforce strong typing and prevent mixing types in your code. It covers all the scenarios you mentioned in your question and provides a stable solution for your requirements.
The above is the detailed content of Can C Help Me Prevent Mixing Apples and Oranges in My Code?. For more information, please follow other related articles on the PHP Chinese website!