Takatoshi Kondo
Takatoshi Kondo

Reputation: 3550

strong typedef of std::string

Motivation (Question background)

I use std::string for many meanings. For example, address and name (in practice more meanings).

Let's say the address and name have a default value.

void set_info(std::string address, std::string name) {
    // set address and name
}
void set_info(std::string address) {
    // set address and set default name
}
void set_info(std::string name) {
    // set name and set default address
}

The target is not only function but also class constructor.

struct info {
    info(std::string address, std::string name)
    : address_{std::move(address)}, name_{std::move(name)}
    {}
    info(std::string address)
    : address_{std::move(address)}
    {}
    info(std::string name)
    : name_{std::move(name)}
    {}
    std::string address_;
    std::string name_;
};

However, the latter two overloads are the same(Only difference is parameter name). So I can't do that.

So I come up with strong typedef of std::string helps the situation.

Additional motivation

If each information has different types, it can avoid accidental type match. I expect that it could help "difficult to misuse" interface. The motivation is something like Boost.Unit https://www.boost.org/doc/libs/1_76_0/doc/html/boost_units.html

Question

Is there any good way to write strong typedef of std::string?

My requirement are here:

  1. Can write overloads of multiple strong typedefs.
    void foo(type1) {}
    void foo(type2) {}
    
  2. Can operate the same as std::string including helper operators. At least operator+() is required. The operator should work similar as https://en.cppreference.com/w/cpp/string/basic_string/operator%2B The return value of the operator+() should be the strong typedef-ed type, not std::string.

I defined the following classes:

struct type1 : std::string {
    using std::string::string;
};

struct type2 : std::string {
    using std::string::string;
};

It works well for the requirement 1. However, the return type of operator+() is std::string, not strong typedef-ed type.

https://wandbox.org/permlink/6GAlcCoZi8XvumnZ

#include <string>
#include <iostream>

struct type1 : std::string {
    using std::string::string;
};

struct type2 : std::string {
    using std::string::string;
};

inline void foo(type1 const& v) {
    std::cout << "type1:" << v << std::endl;
}

inline void foo(type2 const& v) {
    std::cout << "type2:" << v << std::endl;
}

int main() {
    type1 t1 = "ABC";
    type2 t2 = "DEF";
    foo(t1);
    foo(t2);

    // The return type of t1 + "123" is std::string, not type1
    static_assert(std::is_same_v<decltype(t1 + "123"), std::string>);
    // So it is not valid code to call foo(type1 const&)
    // foo(t1 + "123");
}

I'm looking for a way to implement the operator+() that satisfies the following code:

int main() {
    using namespace std::string_literals;
    type1 m1 = "abc";
    type2 m2 = "def";

    // expected foo(type1 const&) is called
    foo(m1);
    foo(m1 + "a");
    foo("a" + m1);
    foo(m1 + "a"s);
    foo("a"s + m1);

    // expected foo(type2 const&) is called
    foo(m2);
    foo(m2 + "a");
    foo("a" + m2);
    foo(m2 + "a"s);
    foo("a"s + m2);

    std::string s = "a"s + "b"; // not become error
}

I tried to implement something like following operator+() but it causes ambiguous overloads.

template <typename T>
inline 
std::enable_if_t<
    std::is_convertible_v<T, std::string> &&
    (!std::is_base_of_v<std::string, std::decay_t<T>> ||
     std::is_same_v<std::decay_t<T>, std::string>),
    type1
>
operator+(type1 lhs, T&& rhs) {
    lhs += std::forward<T>(rhs);
    return lhs;
}

template <typename T>
inline 
std::enable_if_t<
    std::is_convertible_v<T, std::string> &&
    (!std::is_base_of_v<std::string, std::decay_t<T>> ||
     std::is_same_v<std::decay_t<T>, std::string>),
    type1
>
operator+(T&& lhs, type1 const& rhs) {
    type1 ret{std::forward<T>(lhs)};
    ret += rhs;
    return ret;
}

I guess that if I implement all combinations of operator+() overloads for each strong typedef-ed types (type1, type2,...) but I expect that there is better way.

Upvotes: 1

Views: 868

Answers (2)

ropieur
ropieur

Reputation: 130

I use private inheritance to achieve this goal. I need to add using statements on base members I want to see public. This can be seen as a burden, or a design advantage by which you can control what you want to expose. My 2 Cents.

Upvotes: 0

eerorika
eerorika

Reputation: 238361

You won't need to wrap every operation of std::string because you won't need to use them. All that the caller has to do is initialise the argument with a correct type with a value for the internal string:

struct address_type {
    std::string value;
};

struct name_type {
    std::string value;
};

struct info {
    info(address_type address, name_type name)
    : address_{std::move(address.value)}, name_{std::move(name.value)}
    {}
    info(address_type address)
    : address_{std::move(address.value)}
    {}
    info(name_type name)
    : name_{std::move(name.value)}
    {}
private:
    std::string address_;
    std::string name_;
};


info with_address {address_type{"str"}};
info with_name    {name_type   {"str"}};

But if you want to proceed with your approach of providing string functionality with your custom type, then you must wrap all the operations. That's going to be a ton of boilerplate and there's no magic to avoid that.

Upvotes: 3

Related Questions