Поддержка двух целочисленных конструкторов с различной семантикой - PullRequest
0 голосов
/ 23 ноября 2018

Я проектирую класс, который имеет два конструктора с единственными целочисленными аргументами, которые имеют отдельную семантику.

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 должен быть целым типом в этом случае, он можетдаже быть псевдонимом long (в этом случае это даже не скомпилируется).Даже если это не псевдоним типа long, я не уверен, как целочисленные продвижения могут запутать два конструктора.

Идея в том, что я хочу поддержать конструирование пользователя, предоставив базовое представление, или , предоставив ему любое возможное числовое представление, которое затем преобразуется в базовое представление (AKA value_t).

Каков наилучший способ разделения конструкторов?

Ответы [ 3 ]

0 голосов
/ 23 ноября 2018

Я бы порекомендовал сделать конструкторы private и использовать вместо этого статические функции для "создания" ваших экземпляров.Статические функции могут иметь осмысленные имена, чтобы явно указывать пользователям, что ожидать от каждого из них:

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);
    }
};

И даже если вы все еще хотите использовать эти конструкторы вместо назначения внутренней переменной, вы можете поместитьих в приватном разделе и вызывайте из функций static, поэтому пользователи никогда не смогут вызывать их напрямую.

0 голосов
/ 23 ноября 2018

Я разрабатываю класс, который имеет два конструктора с однозначными целочисленными аргументами, которые имеют отдельную семантику.

Каков наилучший способ разделения конструкторов?


Ваш код должен отражать вашу семантику.


Когда кто-то видит интерфейс вашего класса, он должен немедленно угадать, каково его правильное использование.Поскольку я не могу понять семантику, лежащую в основе вашего варианта использования, я сделаю свой собственный, чтобы проиллюстрировать это.

Простой класс для обработки масс

Представьте себе следующий класс для обработки масс;мы расширим его, чтобы обеспечить дополнительную конструктивность:

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

Использование:

#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 демо .

А теперь сделайте так, чтобы он могобрабатывать странные единицы

Теперь я бы хотел, чтобы пользователь мог просто построить Mass из фунтов (или камня, или чего-то еще):

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 }
};

Демонстрация в реальном времени .

Это не работает, поскольку T может быть double.

Сделать семантику очевидной

Решение так же просто, как присвоить имя,Это особенно верно для функций, принимающих несколько аргументов одного типа.Представьте себе, что однажды вы читаете какой-то неясный старый код и натыкаетесь на:

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

Что это делает?Ну, очевидно, это что-то привлекает.Требуется четыре двойных параметра, это может быть прямоугольник.Вы потратили некоторое время, посмотрите объявление draw, найдите его документ, ... и выясните, что он рисует прямоугольник с двумя точками.Это могла быть одна точка, ширина и высота, это могло бы быть много вещей.

В другой жизни представьте вместо строки, которую вы нашли:

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

Это так?не очевидно сейчас?

Делая семантику очевидной в нашем массовом случае

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; }
};

Пользователь может очевидно использовать это так:

#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";
}

Демонстрация в реальном времени .

Улучшения

Теперь, когда вы назвали свою семантику, вы можете пойти дальше и предоставить богатый набор перегрузки:

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 .

Важно отметить, что логика (преобразование единиц) все еще находится в реализации Mass;pounds, stone, и т. Д. - это просто названия: семантика.В этом контексте это может не иметь значения (один килограмм останется ~ 0,16 камня в течение длительного времени), но в целом вы должны предпочесть инкапсулировать эти детали реализации в одном месте.

0 голосов
/ 23 ноября 2018

Я настоятельно рекомендую переосмыслить дизайн.Зачем вам два конструктора, которые выглядят одинаково, но делают две разные вещи?

В любом случае, вы можете использовать тег:

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) {  }
};

Использование

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