基本数据类型的包装类

6
在设计解决方案时,有时为原始数据类型提供包装类可能是方便的。考虑一个表示数值的类,无论是double、float还是int。
class Number {
private:
    double val;

public:
    Number(int n) : val(n) { }
    Number(float n) : val(n) { }
    Number(double n) : val(n) { }

    // Assume copy constructors and assignment operators exist

    Number& add(const Number& other) {
        val += other.val;
        return *this;
    }

    int to_int() const { return (int) val; }
    float to_float() const { return (float) val; }
    double to_double() const { return val; }
};

现在假设我有一个这样的函数:
void advanced_increment(Number& n) {
    n.add(1);
}

我会这样使用这个函数:
Number n(2);
advanced_increment(n); // n = 3

这听起来很容易。但如果函数是这样的呢?
void primitive_increment(int& n) {
    ++n;
}

请注意,这里的增量仅是一个示例。假设该函数将在原始数据类型上执行更复杂的操作,而它们也应该能够在Number类型上执行而不会出现任何问题。
我应该如何像之前一样使用该函数呢?就像下面这样:
Number n(2);
primitive_increment(n);

如何使我的Number类与primitive_increment兼容?我如何创建一个包装器类来支持原始数据类型,在需要这些数据类型的任何地方都可用?
到目前为止,我只发现了两个解决方法。一种是创建一个函数,例如double& Number::get_value(),然后像这样使用它:primitive_increment(n.get_value());。第二种解决方案是创建隐式转换方法,例如Number::operator int&();但是这可能会导致许多模棱两可的调用,并使代码变得混乱。
我想知道是否有其他解决方案来实现这些类型的包装器并保留其原始功能。
更新:
为了进一步澄清,在实际项目中,这里的意图是使所有数据类型都派生自一个基类,通常在设计此类解决方案时称为Object。一个限制是不应使用外部库。因此,如果我有一个包含指向类型Object的指针的容器,它应该能够保存任意的值,原始或非原始,并执行可以在Object上允许的任何原始操作。我希望这样可以更好地解释。

2
直接使用原始数据类型有什么问题? - Pubby
当您将Number类传递到primitive_increment中时,您实际上希望该值发生什么?具体而言,当它当前具有非整数值(例如2.5)或超出int范围的值时会发生什么?在您决定语义之前,无法对此问题提供正确的答案。 - Oliver Charlesworth
2
@teedayf:我认为你需要一个更好的例子!每个人都在处理原始类型之间的转换问题上卡住了。你真的只是想要(例如)一个“Int”类,可以在任何可以使用“int”的地方使用吗? - Oliver Charlesworth
2
@teedayf:我非常怀疑,你最好为原始数据类型提供单独的特化或重载,而不是试图将它们强行塞入某些虚拟多态层次结构中。有人在1995年认为这是个好主意,结果我们得到了Java。 - Kerrek SB
@KerrekSB,哈哈,所以我猜答案是C++并不适合这样的事情。好吧,我想我已经尽可能地回答了这个问题。对于最初的混淆,我感到抱歉。如果有其他人投票关闭这个问题,我会很高兴的。 - Zeenobit
显示剩余10条评论
7个回答

1
class Number {
    enum ValType {DoubleType, IntType} CurType;
    union {
        double DoubleVal;
        int IntVal;
    };
public:
    Number(int n) : IntVal(n), CurType(int) { }
    Number(float n) : DoubleVal(n), CurType(DoubleType) { }
    Number(double n) : DoubleVal(n), CurType(DoubleType) { }

   // Assume copy constructors and assignment operators exist

    Number& add(const Number& other) {
        switch(CurType) {
        case DoubleType: DoubleVal += other.to_double(); break;
        case IntType: IntVal+= other.to_int(); break;
        }
        return *this;
    }

    int& to_int() { 
        switch(CurType) {
        case DoubleType: IntVal = DoubleVal; CurType = IntType; break;
        //case IntType: DoubleVal = IntVal; CurType = DoubleType; break;
        }
        return IntVal; 
    }
    const int to_int() const { 
        switch(CurType) {
        case DoubleType: return (int)DoubleVal;
        case IntType: return (int)IntVal;
        }
    }
    const float to_float() const { 
        switch(CurType) {
        case DoubleType: return (float)DoubleVal;
        case IntType: return (float)IntVal;
        }
    }

    double& to_double() { 
        switch(CurType) {
        //case DoubleType: IntVal = DoubleVal; CurType = IntType; break;
        case IntType: DoubleVal = IntVal; CurType = DoubleType; break;
        }
        return DoubleVal; 
    }
    const double to_double() const { 
        switch(CurType) {
        case DoubleType: return (double)DoubleVal;
        case IntType: return (double)IntVal;
        }
    }
};

void primitive_increment(int& n) {
    ++n;
}

int main() {
    Number pi(3.1415);
    primitive_increment(pi.to_int());
    //pi now is 4
    return 0;
}

我承认这很尴尬,也不是理想的情况,但它解决了给定的问题。


非常。我认为真正的答案是将函数模板化。 - Mooing Duck

1

C++11具有显式的运算符重载。

struct silly_wrapper {
  int foo;
  explicit operator int&() { return foo; }
};

void primitive_increment(int& x) { ++x; }


int main()
{
   silly_wrapper x;
   primitive_increment(x); // works
   x += 1; // doesn't work - can't implicitly cast
}

看起来比较复杂,因为他想让 foobar 相等。 - Mooing Duck
@MooingDuck 复杂吗?我会移除 bar - 我正在展示如何创建显式转换重载,而不是如何创建联合。 - Pubby
哦,我错过了“显式”的词。这改变了事情的性质。如果那个开头的文本也在那里,我也错过了它。 - Mooing Duck

0

不要将其提供给primitive_increment。你应该为你的Number类重载++运算符,并通过这种方式进行递增。

Number& operator++() { ++val; return *this;}
Number& operator+=(const Number& rhs) { val += rhs.Val; return *this;}
Number operator+(const Number& rhs) { Number t(*this); t+=rhs; return t;}

请参见:C和C++中的运算符


递增函数只是一个简单的例子。假设其他复杂的函数将由这些函数执行。 - Zeenobit
的确。虽然惯例是基于operator+=定义operator+ - Oliver Charlesworth
@teedayf:我认为OP想知道是否可以重载运算符。 - Mooing Duck
@MooingDuck,我非常清楚我可以为包装类重载所有有效的运算符(无论如何我都会这样做)。但这并没有解决问题,因为我仍然无法直接将“Number”对象无缝地传递给“primitive_increment”。 - Zeenobit
@teedayf:没有意识到目的是将其传递给一个接受“int”的函数。 - Mooing Duck

0
将转换运算符设为私有,并在友元函数内进行转换。
class silly_wrapper {
private:
  int foo;
  float bar;
  operator int&() { return foo; }

  template <typename T>
  friend void primitive_increment(T& x) { ++static_cast<int&>(x); }
};


int main()
{
   silly_wrapper x;
   primitive_increment(x); // works

   int i;
   primitive_increment(i); // works

   int& r = static_cast<int&>(x); // can't convert - operator is private
}

重点是包装类需要传递给任意一组函数。 - Oliver Charlesworth

0

这是我刚想到的一个更加奇怪的答案:

class Number; 
template<class par, class base>
class NumberProxy {
    base Val;
    par* parent;
    NumberProxy(par* p, base v) :parent(p), Val(v) {}
    NumberProxy(const NumberProxy& rhs) :parent(rhs.parent), Val(rhs.Val) {}
    ~NumberProxy() { *parent = Val; }
    NumberProxy& operator=(const NumberProxy& rhs) {Val = rhs.Val; return *this}
    operator base& {return Val;}
};

class Number {
private:
    double val;
public:
    Number(int n) : val(n) { }
    Number(float n) : val(n) { }
    Number(double n) : val(n) { }
    // Assume copy constructors and assignment operators exist        
    int to_int() const { return (int) val; }
    float to_float() const { return (float) val; }
    double to_double() const { return val; }

    NumberProxy<Number,int> to_int() { return NumberProxy<Number,int>(this,val); }
    NumberProxy<Number,float> to_float() { return NumberProxy<Number,float>(this,val); }
    NumberProxy<Number,double> to_double() { return NumberProxy<Number,double>(this,val); }
};

void primitive_increment(int& n) {
    ++n;
}

int main() {
    Number pi(3.1415);
    primitive_increment(pi.to_int());
    //pi now is 4
    return 0;
}

Number.to_int() 返回一个 NumberProxy<int>,它可以隐式转换为一个 int&,函数就在这个上面操作。当函数和表达式完成后,临时的 NumberProxy<int> 就会被销毁,它的析构函数会更新它的父类 Number 的值。这样做的好处是只需要对 Number 类进行轻微修改。

显然,这里存在一些危险,如果在同一语句中调用两次 to_N(),那么这两个 int& 将不会同步,或者如果有人将 int& 带到语句的末尾。


这也可能具有危险的语义。考虑 Number x(3); int &r = x.to_int(); r = 4; - Oliver Charlesworth
我忘了在答案中提到这一点。已编辑。 - Mooing Duck

0

这有点冒险,因为我不完全确定你的整体设计如何配合在一起。

那么模板化的自由函数怎么样:

class IncTagIntegral{};
class IncTagNonintegral{};
template <bool> struct IncTag { typedef IncTagNonintegral type; }
template <> struct IncTag<true> { typedef IncTagIntegral type; }

template <typename T> void inc_impl(T & x, IncTagIntegral)
{
  ++x;
}

template <typename T> void inc_impl(T & x, IncTagNonintegral)
{
  x += T(1);
}


template <typename T> void primitive_increment(T & x)
{
  inc_impl<T>(x, typename IncTag<std::is_integral<T>::value>::type());
}

template <> void primitive_increment(Number & x)
{
  // whatever
}

这种方法可能适用于您需要同时应用于现有类型和自己的类型的其他函数。


这里有另一种可能性,这次使用类型擦除:

struct TEBase
{
   virtual void inc() = 0;
}

struct any
{
  template <typename T> any(const T &);
  void inc() { impl->inc(); }
private:
  TEBase * impl;
};

template <typename T> struct TEImpl : public TEBase
{
  virtual void inc() { /* implement */ }
  // ...
}; // and provide specializations!

template <typename T> any::any<T>(const T & t) : impl(new TEImpl<T>(t)) { }

关键在于您可以通过特化提供不同的 TEImpl<T>::inc() 具体实现,但是对于类型为 any任何 对象 a 都可以使用 a.inc()。 您可以基于这个想法构建其他自由函数包装器,例如 void inc(any & a) { a.inc(); }

他的目标似乎是将他的“Number”类传递给期望“int”的任意函数。 - Mooing Duck
@MooingDuck:很有可能。我不能说我完全支持这个项目。OP可以详细说明,并提供更具说明性的示例。 - Kerrek SB
@KerrekSB,实际上,在这个项目中,意图是让所有数据类型都派生自一个基类,通常在设计此类解决方案时称为“Object”。一个限制条件是不能使用任何外部库。因此,如果我有一个容器,其中包含指向类型“Object”的指针,它应该能够容纳任意的值,原始或非原始,并执行任何允许在“Object”上执行的原始操作。我希望这样更好地解释了。 - Zeenobit
@teedayf:嗯...那解释了你想做的一方面(虽然不知道为什么这是个好主意),但并没有说明它与运算符和函数的联系。你是否希望所有函数都可以神奇地作用于从你的超级对象派生出来的所有东西? - Kerrek SB
@KerrekSB:不是魔法,但在某种程度上是可以的。我希望这对用户来说是不可见的。 - Zeenobit

0
如果你的Number类没有实现int的子集,那么你就不能这样做。例如,如果你的Number类包含值INT_MAX并且可以容纳值INT_MAX+1,那么它会给出错误的结果。如果你的Number类模拟了int的子集,那么转换为int再转回来当然是一个选项。
除此之外,你唯一的机会就是重写函数以接受Number对象。最好将其作为模板,这样它既可以与int一起工作,也可以与Number(以及任何其他当前或未来呈现int-like接口的类)一起工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接