I have to define a few structs that contain a member of type std::string. I need to make them behave like std::string. In other words, they should be trivially convertible to std::string but not to each other. Instances of these structs will in turn be members of another class:
class Foo
{
Name_t m_name;
Description_t m_description;
// ...
};
Thus a function taking a parameter of type Name_t should not be callable with a Description_t instance even though they're both std::string under the hood.
How can this be achieved in a proper way? I tried overloading the operator std::string& for all the those types. It seems to work though. And I am not sure if I should declare them with the explicit keyword.
An example:
#include <type_traits>
#include <utility>
#include <string>
#include <cassert>
struct Description_t
{
std::string value;
operator std::string&( ) noexcept
{
return value;
}
operator const std::string&( ) const noexcept
{
return value;
}
};
bool operator==( const Description_t& lhs, const Description_t& rhs ) noexcept
{
// This using declaration looks too verbose to my eyes!
using underlying_type = std::add_lvalue_reference_t<
std::add_const_t<
decltype( std::declval<decltype( lhs )>( ).value )> >;
return static_cast<underlying_type>( lhs ) == static_cast<underlying_type>( rhs );
// return lhs.value == rhs.value; // equivalent to the above statement
}
struct Name_t
{
std::string value;
operator std::string&( ) noexcept
{
return value;
}
operator const std::string&( ) const noexcept
{
return value;
}
};
bool operator==( const Name_t& lhs, const Name_t& rhs ) noexcept
{
// Again, this using declaration looks too verbose to my eyes!
using underlying_type = std::add_lvalue_reference_t<
std::add_const_t<
decltype( std::declval<decltype( lhs )>( ).value )> >;
return static_cast<underlying_type>( lhs ) == static_cast<underlying_type>( rhs );
// return lhs.value == rhs.value; // equivalent to the above statement
}
int main()
{
Description_t stock1 { "Hi!" };
Description_t stock2 { "Hi!" };
Name_t name1 { "Hello!" };
Name_t name2 { "Hello!" };
assert( name1 == name2 );
// assert( name1 == stock1 );
}
However, in the definition for underlying_type I had to do a static_cast to const std::string& by adding ref and const to it. Otherwise, the compiler would generate two inefficient copy constructions inside the operator== to compare the values of the two Name_t arguments. Is there a more concise way of doing this (other than lhs.value == rhs.value)?
Also should the operator== be a member function in the above cases?
If you want a typed string that behaves like a string, CRTP with std::string inheritance would be an option:
A
std::stringwill always convert toDescriptionand toNamebut noDescriptionwill convert toNameorstd::string(or vice versa).As suggested in the comments by 0xbachmann you can be even less verbose by using lambda types instead of CRTP:
But then you wont be able to add custom members, obviously.