Supporting two integer constructors with different semantics

145 Views Asked by At

I am designing a class that has two constructors with single integer arguments that have separate semantics.

template <typename value_t>
class Numeric {
    value_t value_;
public:
    Numeric(value_t value) : value_(value) {}
    Numeric(long l) { /* computes value_ from l */ }
    Numeric(double d) { /* computes value_ from d */ }
    Numeric(std::string bstring) { /* computes value_ from binary string */ }
    // ...
};

value_t must be an integral type in this case, it could even be an alias to long (in which case this wouldn't even compile). Even if it wasn't a type alias to long, I'm not so sure how integer promotions might confuse the two constructors.

The idea is I want to support the user constructing by giving the underlying representation, or by giving it any possible numeric representation, which would then convert to the underlying representation (A.K.A value_t).

What's the best way to segregate the constructors?

3

There are 3 best solutions below

8
On BEST ANSWER

I would recommend making the constructors private and using instead static functions to "create" your instances. The static functions can have meaningful names, in order to explicitly tell the users what to expect from each:

template <typename value_t>
class Example {
    value_t value_;
    //Prevent users to directly create instances
    Example(value_t value): value_(value)
    {
    }
public:
    static Example createFromValue(value_t value)
    {
        return Example(value);
    }
    static Example createComputingValueFromLong(long l)
    {
        return Example(/*Compute from l*/l);
    }
};

And even if you still want to use those constructors instead of assigning the internal variable, you can put them in the private section and call them from the static functions, so the users will never be able to call them directly.

6
On

I would strongly suggest you to rethink the design. Why do you need two constructors that look the same, but do two different things?

Anyhow, you could use a tag:

struct tag_a{};
struct tag_b{};

template <typename value_t>
class Example {
    value_t value_;
public:
    Example(value_t value, tag_a) : value_(value) {}
    Example(long l, tag_b) {  }
};

Usage

long x = 123;
auto a = Example<long>(x,tag_a());
auto b = Example<long>(x,tag_b());
1
On

I am designing a class that has two constructors with single integer arguments that have separate semantics.

What's the best way to segregate the constructors?


Your code should reflect your semantics.


When someone sees the interface of your class, they must immediately guess what its correct usage is. Since I cannot know the semantic behind your use case, I'll make my own to illustrate that.

A simple class to handle masses

Imagine the following class to handle masses; we will enhance it to provide additional constructability:

template<class T>
class Mass
{
    T _kg;
public:
    Mass(T kg) : _kg(kg) {}
    T kg() const { return _kg; }
};

Usage:

#include <iostream>
int main()
{
    Mass<double> one_litter_of_water(1.0);
    std::cout << "1 L of water is: " << one_litter_of_water.kg() << " kg\n";
}

Live demo.

And now make it able to handle weird units

Now, I'd like a simple way for the user to construct a Mass from pounds, (or stone, or whatever):

template<class T>
class Mass
{
    T _kg;
public:
    Mass(T kg) : _kg(kg) {}
    Mass(double lb) : _kg(lb/2.2046) {} // ho no!
    T kg() const { return _kg }
};

Live demo.

This doesn't work since T can be double.

Make semantic obvious

The solution is as simple as naming thing. This is particularly true for functions taking multiple arguments of the same type. Imagine someday you read some obscure old code and you stumble upon:

draw(2.6, 2.8, 54.1, 26.0); // draw selection

What does it do? Well, obviously it draws something. It takes four double parameters, it may be a rectangle. You take some time, go see the declaration of draw, find its doc, ... and figure out it draws a rectangle given two points. It could have been one point, a width and a height, it could have been many things.

In another life, imagine instead of the line above you found:

draw(Point{2.6, 2.8}, Point{54.1, 26.0}); // draw selection

Is it not obvious now?

Making semantic obvious in our mass case

struct pounds { double value; operator double() const { return value; } };

template<class T>
class Mass
{
    T _kg;
public:
    Mass(T kg) : _kg(kg) {}
    Mass(pounds lb) : _kg(lb/2.2046) {}
    T kg() const { return _kg; }
};

The user can obviously use it like this:

#include <iostream>
int main()
{
    Mass<double> one_litter_of_water(pounds{2.2046});
    std::cout << "1 L of water is: " << one_litter_of_water.kg() << " kg\n";
}

Live demo.

Improvements

Now that you've named your semantics, you can go further and provide a rich overload set:

struct unit { double value; operator double() const { return value; } };
struct pounds : unit {};
struct stones : unit {};
struct grams  : unit {};

template<class T>
class Mass
{
    T _kg;
public:
    Mass(T kg) : _kg(kg) {}
    Mass(pounds lb) : _kg(lb/2.2046) {}
    Mass(stones st) : _kg(st/0.1575) {}
    Mass(grams  g)  : _kg(g/1000.0) {}
    T kg() const { return _kg; }
};

Live demo.

This is important to note that the logic (unit conversion) is still in the implementation of Mass; pounds, stone, etc. are just names: semantics. In this context it may not matter (one kilogram will stay ~0.16 stones for a long time), but in general you should prefer to encapsule those implementation details at the same place.