【问题标题】:Supporting two integer constructors with different semantics支持两个具有不同语义的整数构造函数
【发布时间】:2018-11-23 16:10:43
【问题描述】:

我正在设计一个具有两个构造函数的类,该构造函数带有具有不同语义的单个整数参数。

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 的类型别名,我也不太确定整数提升会如何混淆这两个构造函数。

我的想法是我想通过提供基础表示来支持用户构建,通过给它任何可能的数字表示,然后将其转换为基础表示(A.K.A value_t)。

分离构造函数的最佳方法是什么?

【问题讨论】:

  • 选项要么使用一个构造函数为long 定义特化,要么更改构造函数的签名。这些是您在 C++ 中的唯一选择。
  • 为什么不创建一个函数来“从 long 中计算 value_”,然后将其传递给类?
  • @hlscalon 这不是构造函数的重点吗?取值并计算和分配成员?我只是也试图支持成员的直接初始化。
  • 构造函数的工作不可能是关心您可以对其参数进行的所有可能的转换。如果您需要long 来构造实例,则构造函数应采用long
  • @ktb 构造函数的重点是构造一个对象。从代码中可以明显看出,仅使用转换的 result 即可完全构造对象,因此无需在构造函数中执行该计算。您可以只需要该结果,并在需要时提供一种执行转换的方法。

标签: c++ constructor constructor-overloading


【解决方案1】:

我强烈建议您重新考虑设计。为什么你需要两个看起来一样的构造函数,但是做两件不同的事情?

无论如何,你可以使用标签:

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

【讨论】:

  • 之所以有两个不同的构造函数是一个直接初始化value_成员,但是该类也可以将整数转换为存储在value_中的适当的底层表示。 value_t 构造函数很可能是私有的。
  • @ktb 然后就不需要公共构造函数的标签。
  • @ktb 我不太明白你的推理。如果value_t 有一个私有构造函数,那么Example 的第二个构造函数不能工作(或者它是否也使用初始化列表?在这种情况下我仍然不明白为什么它必须是两个构造函数......)
  • 我看不出第二个构造函数是如何工作的?想象一下身体就像value_ = l * 2
  • @ktb 如果value_ts 构造函数是私有的,那么你不能构造该成员(因此也不在构造函数体中为其赋值)
【解决方案2】:

我建议创建构造函数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 函数中调用它们,这样用户将永远无法直接调用它们。

【讨论】:

  • 不能默认构造是一个相当大的限制。我认为这样做不值得。您可以使用采用 long 和公共静态 createFromX 方法的公共构造函数获得相同的结果
  • OP 代码还不是默认可构造的,所以它不应该是一个问题......无论如何,是的,它可以有一个默认构造函数和其他静态函数来解决不同的情况
  • @user463035818 这就是我要做的。除了long 构造函数之外,还有很多其他的东西。该类是一个数字类型,可以从任何可能的数字类型(我能想到的)构造。但是value_t 必须是分开的,createFromValue 似乎是一个好主意,就像标签一样,但直接在调用中。
  • 我想展示如何仅使用静态函数来解决它,而不是只关注构造函数,因为 OP 过于关注使用构造函数而不是思考如何解决似乎是一个问题它的实际问题
  • @ktb“不仅仅是一个长构造函数”那么也许你应该在问题中包含它......我不知道,但也许它是一个xy problem。为您想要“转换”的每种单一类型提供一个构造函数可能是要走的路,但听起来有点可疑
【解决方案3】:

我正在设计一个具有两个构造函数的类,该构造函数具有单个整数参数,它们具有不同的语义。

分离构造函数的最佳方法是什么?


您的代码应该反映您的语义。


当有人看到你的类的接口时,他们必须立即猜出它的正确用法是什么。由于我不知道您的用例背后的语义,因此我将自己进行说明。

处理群众的简单类

想象下面的类来处理群众;我们将对其进行增强以提供额外的可构建性:

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 demo.

现在让它能够处理奇怪的单位

现在,我想要一种简单的方法让用户用磅(或石头或其他东西)构造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 }
};

Live demo.

这不起作用,因为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";
}

Live demo.

改进

现在您已经命名了语义,您可以进一步提供丰富的重载集:

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的实现中; poundsstone只是名称:语义。在这种情况下,这可能无关紧要(一公斤会在很长一段时间内保持约 0.16 块石头),但一般来说,您应该更愿意将这些实施细节封装在同一个地方。

【讨论】:

  • 你一定是从我关于“为什么给东西起正确的名字”的幻灯片中偷了drawPoints 的例子,我实际上从来没有向任何人展示过:P nice回答
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-19
  • 1970-01-01
相关资源
最近更新 更多