3

In a codebase I am working on, most functions takes as arguments several std::string. These strings represent different things, and I would like to define several classes to detect inconsistencies at compile time if possible.

Here is an example of problematic code:

void displayIdentity(const std::string &firstName, const std::string &lastName) {
    std::cout << "First name = " << firstName << "\t; " << "Last name = " << lastName << std::endl;
}

const std::string firstName = "John";
const std::string lastName  = "Doe";
displayIdentity(lastName, firstName); // clearly, it should have been 
                                      // displayIdentity(firstName, lastName)

The current solution I am using is to use different classes defined using template:

template <typename NAME> class TypedString : public std::string {
  public:
    explicit TypedString(const char* s) : std::string{s} {}
  
    explicit TypedString(const std::string& s) : std::string{s} {}
};

Then it becomes possible to detect inconsistency at compile time:

const FirstName firstName2{"John"};
const LastName  lastName2{"Doe"};
displayIdentity2(firstName2, lastName2);
// displayIdentity2(lastName2,  firstName2); // error: invalid initialization of reference of type 
                                             // 'const FirstName&' from expression of type 'LastName'
    
    
//const LastName lastName3{firstName2};      // error: invalid initialization, as expected

const LastName lastName3{std::string(firstName2)}; // but conversion can to requested explicitly 
    

I have started using this code, and it works as expected and helps me detect inconsistencies.

I have read on this post that we should not inherit from std::string because it's destructor is not virtual.

As the TypedString class is empty, it's destructors should be equivalent to the std::string destructor, so I don't see this as problem. Same things for methods, I don't care if std::string methods are called when using polymorphism, because I want TypedString to behave exactly like a string, except for construction/conversion...

Am I correct? Or should I use a more verbose solution (using composition as done here) to solve my problem?

Jarod42
  • 203,559
  • 14
  • 181
  • 302
GabrielGodefroy
  • 198
  • 1
  • 8
  • 2
    boost has a `STRONG_TYPEDEF`. – 463035818_is_not_an_ai Nov 09 '22 at 13:00
  • I don't think composition would hurt. I can even imagine that you might find useful methods you want to have on FirstName vs. Lastname. E.g. our company's internal abbreviations takes the first and last letter of your last name. We don't want this on the first name. – Thomas Weller Nov 09 '22 at 13:05
  • 1
    builder pattern comes to mind. Its much harder to get something along the line of `buildPerson().withFirstName("Smith").withLastName("Peter");`wrong than order or parameters – 463035818_is_not_an_ai Nov 09 '22 at 13:15
  • Rater than use a template parameter, you can use *private inheritance* to prevent polymorphic accidents. – Galik Nov 09 '22 at 13:25
  • Also, there is no reason not to inherit from `std::string` for creating a distinct type. – Galik Nov 09 '22 at 13:29

2 Answers2

2

As long as you don't delete any instances of your TypedString via a pointer to std::string the lack of a virtual destructor is irrelevant.

That is, the behavior of the following is undefined since std::string's destructor is not virtual:

std::string* name = new FirstName{"John"};
delete name;

If you never use your class in that way you won't have any problems. It may be worth using private inheritance and explicitly exposing the member functions you need. That would prevent the snippet above from compiling at all, but it may also make explicit desired conversions more difficult or verbose.

Miles Budnek
  • 28,216
  • 2
  • 35
  • 52
0

Please don't. Amongst others, std::string dtor is not virtual, type is not polymorphic; you'd only save some typing with that. If you do a generic template<typename T> class StrongType that works for any type and simply delegates everything unless specialized, then you only type it once and you can do all the specializations, etc.

lorro
  • 10,687
  • 23
  • 36
  • 2
    As long as OP doesn't try to access a derived through a base, it's not an issue (i.e. as long as OP does not use it polymorphically, we don't care the base class doesn't have a virtual destructor) – Fareanor Nov 09 '22 at 13:23
  • @Fareanor As soon as user would like to have a member beside `std::string` that has a non-trivial dtor, it might be a problem to delete `base*` containing `derived*`. – lorro Nov 09 '22 at 13:31
  • 2
    I don't understand what you mean. Why are you talking about a _"member beside `std::string`"_ ? The point was that it's perfectly fine to inherit from `std::string` if we don't mean to use the custom string class polymorphically (i.e. we don't try to access the custom string class though a pointer/reference to a `std::string`). – Fareanor Nov 09 '22 at 13:36
  • These days, not everyone uses a *virtual destructor* when dealing with *inheritance based polymorphism*. Smart pointers can be used to ensure the correct *destructor* gets called on deletion. – Galik Nov 09 '22 at 20:05
  • @Galik Yes, I'm aware that `shared_ptr<>` works without virtual dtor (if assigned at construction). Same thing does not hold for e.g. `unique_ptr<>` unless a deleter is manually provided. – lorro Nov 09 '22 at 20:37