命名构造函数技巧和new运算符

22
我使用命名构造函数惯用语来创建对象,因为我有很多调用都具有相同的参数,但是对象应该以不同的方式创建。 C ++ FAQ告诉我们如何做到这一点。它还告诉我们如何强制对象在堆上分配。然而,它确实没有告诉我们如何使用new运算符与命名构造函数惯用语。
因为new需要调用构造函数,我们不能直接调用命名构造函数。所以,我找到了两个解决这个问题的方法:
我创建了一个额外的复制构造函数,并希望优化编译器不会创建临时对象。
class point_t {
    int X,Y;
    point_t(int x, int y) : X(x), Y(y) { }
  public:
    point_t(const point_t &x) : X(x.X), Y(x.Y) { }
    static point_t carthesian(int x, int y) { return point_t(x,y); }
    static point_t polar(float radius, float angle) {
      return point_t(radius*std::cos(angle), radius*std::sin(angle));
    }

    void add(int x, int y) { X += x; Y += y; }
};



int main(int argc, char **argv) {
  /* XXX: hope that compiler doesn't create a temporary */
  point_t *x = new point_t(point_t::carthesian(1,2));
  x->add(1,2);
}

另一种版本是创建单独的命名构造函数。由于函数重载不适用于返回类型,我使用了两个不同的名称,这很丑陋。

class point_t {
    int X,Y;
    point_t(int x, int y) : X(x), Y(y) { }
  public:
    /* XXX: function overloading doesn't work on return types */
    static point_t carthesian(int x, int y) { return point_t(x,y); }
    static point_t *carthesian_heap(int x, int y) { return new point_t(x,y); }
    void add(int x, int y) { X += x; Y += y; }
};

int main(int argc, char **argv) {
  point_t *x = point_t::carthesian_heap(1,2);
  x->add(1,2);
}

有没有一个更美观的版本与示例代码相等?


1
使您的类具有可移动性,第一个代码不会导致任何副本(并不是两个整数很重要)。另外,您真的需要堆分配吗? - Cat Plus Plus
术语“movable”是什么意思?实际的类比这个要大得多,创建副本会导致明显的惩罚。这就是为什么它大多数时候应该在堆上分配,但有时栈分配也很有用。 - Alexander Oh
1
Moveable是C++11的概念——学习rvalue引用(新的&&语法)。我认为它会让你得到想要的结果——如果类是可移动的,那么它可以传递给复制构造函数,但实际上只是在原地初始化。如果您还不能使用C++11,那么我认为您就卡住了。没有任何提出的解决方案能够真正让您得到想要的结果,它们只是重新排列代码。 - mgiuca
5个回答

16

你完全可以避免使用命名构造函数惯用法,而是使用额外的枚举参数来选择构造函数。

enum Carthesian {carthesian};
enum Polar {polar};
class point_t {
    int X,Y;
  public:
    point_t(int x, int y) : X(x), Y(y) { } // may keep as a default
    point_t(Carthesian, int x, int y) :X(x),Y(y){}
    point_t(Polar, float radius, float angle)
    : X (radius*std::cos(angle)), Y(radius*std::sin(angle)) {}
    void add(int x, int y) { X += x; Y += y; }
};

int main(int argc, char **argv) {
  point_t *x = new point_t(carthesian,1,2);
  point_t *y = new point_t(polar,0,3);
  x->add(1,2);
}

它很简单、易于移植,而你唯一可能遇到的开销就是传递虚拟枚举值。在极少数情况下,如果这个开销对你来说太高,即使构造函数本身没有被内联,也可以通过包装函数调用来消除它,如下所示:

enum Carthesian {carthesian};
enum Polar {polar};
class point_t {
    int X,Y;
    void initCarthesian(int x, int y); // may be long, not inlined
    void initPolar(float radius, float angle);
  public:
    point_t(int x, int y) : X(x), Y(y) { } // may keep as a default
    point_t(Carthesian, int x, int y)
    {initCarthesian(x,y);} // this is short and inlined
    point_t(Polar, float radius, float angle) {initPolar(radius, angle);}
    void add(int x, int y) { X += x; Y += y; }
};

另一种方法是使用派生类进行构造。当使用内部类时,我认为这会导致相当好的语法:

class point_t {
    int X,Y;
  public:
    struct carthesian;
    struct polar;
    point_t(int x, int y) : X(x), Y(y) { } // may keep as a default
    void add(int x, int y) { X += x; Y += y; }
};

struct point_t::carthesian: public point_t
{
  carthesian(int x, int y):point_t(x,y){}
};

struct point_t::polar: public point_t
{
  polar(float radius, float angle):point_t(radius*std::cos(angle),radius*std::sin(angle)){}
};

int main(int argc, char **argv) {
  point_t *x = new point_t::carthesian(1,2);
  point_t *y = new point_t::polar(0,3);
  x->add(1,2);
  return 0;
}

2
一个类似的方法是为一些构造函数参数引入特定类型。例如,您可以定义一个半径类和一个角度类,它们都有一个显式构造函数,接受一个浮点数并进行浮点数转换。 - Nicola Musatti
要“强制对象在堆上分配内存”,正如OP所提到的(但没有说明是必需的),可以将析构函数设置为“protected”或“private”。最好选择前者。并添加一个销毁函数或友元函数到一个通用的销毁函数。在C++98中,标准库中最接近通用销毁函数的是std::auto_ptr,但它不处理数组。在C++0x中,您有std::unique_ptr机制代替。 - Cheers and hth. - Alf
@Suma:实际上我是指添加类,如Radius和Angle,但你的方法也是有效的。不过我会将point_t构造函数设为protected。 - Nicola Musatti
@Nicola:一个有趣的(对我来说是新颖的)方法。如果你基于它写出自己的答案,我一定会点赞的。 - Suma
@Suma。你的提案比我认为有害的命名构造函数习惯更优越。首先,在使用极坐标时会进行过早的转换,这样做没有任何好处,只会引入数值误差。其次,根据我的经验,选择一个不保留输入数据原始状态的策略会带来很大的风险。迟早会出现需要知道原始定义的要求,到那时就会一团糟了。 - user2656151

8
你可以编写以下内容:

您可以编写:

point_t *x = new point_t(point_t::carthesian(1,2));

首先调用carthesian()函数,然后调用复制构造函数。

那么,这样做有什么问题吗?也许会有点慢?

顺便说一下,这段代码有一个明显的优点:程序员可以清楚地在自己的代码中看到new运算符(他正在使用其他人编写的point_t),因此可以假设他有责任在完成x后调用delete


这里并不一定需要使用复制构造函数。在某些情况下,可以使用移动构造函数,并且对于比 point_t 更复杂的类型,移动构造实际上避免了复制。对于像 point_t 这样的简单类型,编译器应该会优化掉复制操作。因此,不,这不应该很慢。 - Helmut Grohne

4
这真的是一个问题吗?在我看来,类通常要么大部分时间都是动态分配的,要么根本不是。像这里的point_t类一样代表值的类属于第二类,而代表实体(即具有身份的东西)的类属于第一类。
因此,我的建议是选择您认为对每个类最好的方法,并仅提供该方法。请注意,您始终可以返回一个直接分配的小对象,该对象具有指向较大对象的私有指针,如Handle-Body惯用法中所述。
另一方面,其他答案展示了如何在接受相同数量和类型的参数的构造函数之间消除歧义。在这种思路中,另一种替代方法是引入特定类型的参数,如下所示:
class radius_t {
    float R;
  public:
    explicit radius_t(float r) : R(r) {}
    operator float() const { return R; }
};

class angle_t {
    float A;
  public:
    explicit angle_t(float a) : A(a) {}
    operator float() const { return A; }
};

class point_t {
    float X,Y;
  public:
    point_t(float x, float y) : X(x), Y(y) { }
    point_t(radius_t radius, angle_t angle) :
      X(radius*std::cos(angle)), Y((radius*std::sin(angle)) {
    }

    void add(int x, int y) { X += x; Y += y; }
};

int main(int argc, char **argv) {
  point_t *x = new point_t(radius_t(1),angle_t(2));
  x->add(1,2);
}

我打算使用的类是bit_vector,其中含有一些不明确的参数,包括指针和整数。我不想使用std::vector<bool>,因为我使用了运算符重载。这些位向量用于指定指令的二进制编码,因此作为引用传递。另一个使用这个位向量的方法是创建掩码,选择指令的某个特定部分,这经常被更改和复制,因此需要堆栈分配。所以我不能选择其中一个。 - Alexander Oh
@Alex:你所描述的情况对于 Handle-Body 惯用语来说似乎是一个合理的使用案例。 - Nicola Musatti
我没有点踩,只是取消了点赞。原因很简单,虽然句柄-体惯用语看起来很好用于制作对象的浅拷贝,但它并不能真正解决我的问题。我需要堆和栈分配的对象,因为有些实例应该在函数结束时被销毁,而其他实例实际上永远不会被销毁。句柄-体并没有解决这个特定的问题。但是你的答案并不无用,因为我有代码的其他部分,这种惯用语对于减少头文件包含是有用的。 - Alexander Oh
我明白了。我没有意识到取消点赞与点赞+踩的效果相同。无论如何,我理解你的观点,并且我喜欢你选择的答案,尽管我更喜欢提供现有参数的具体类型的方法,正如我在评论中提到的那样。 - Nicola Musatti
我刚看到你编辑了答案,我有点喜欢它,使用转换运算符而不是继承似乎更优越(不需要指针),但我宁愿选择不同的类:class carthesian_t { carthesian_t (float X, float Y) : x(X), y(Y) { } operator point_t() { return point_t(x,y); } }; 和 class radiant_t { ... }; - Alexander Oh

0

我没有看到的一种方法是重载构造函数,使堆分配使用最后一个参数作为输出参数(尽管第二个函数在技术上不是构造函数,它不返回实例)。结果将类似于以下内容(以您的第二个代码片段为基础):

class point_t {
    int X,Y;
    point_t(int x, int y) : X(x), Y(y) { }
  public:
    /* XXX: function overloading doesn't work on return types */
    static point_t carthesian(const int x, const int y) { return point_t(x,y); }
    static void carthesian(const int x, const int y, point_t * & point) { point = new point_t(x,y); }
    void add(int x, int y) { X += x; Y += y; }
    void add(const point_t & point) { this->X += point.x; this->Y += point.y; }
};

int main(int argc, char **argv) {
    point_t p1 = point_t::carthesion(1, 2);
    point_t * p2;
    point_t::carthesian(1, 2, p2);

    p2->add(p1);
}

-1
可以考虑使用模板分配器:
template<typename T>
struct Allocator : T
{
  template<typename A1, typename A2>
  Allocator(A1 a1, A2 a2) : T(a1, a2) {}
};

class point_t {
//...
  template<typename T> friend struct Allocator;
};

int main(int argc, char **argv) {
  point_t *x = new Allocator<point_t>(1,2);
  x->add(1,2);
}

现在Allocatorpoint_tfriend。因此,它可以访问其private构造函数。此外,您可以在Allocator中添加一些构造函数,例如<A1,A2>,以使其更加generalized。优点如下:

  1. 看起来不冗长。
  2. 您不必担心编译器优化
  3. friendship没有被利用,因为Allocator是一个template,我们仅将其用于堆分配

演示


它创建了与OP尝试使用其命名构造函数解决的相同问题。 (1,2)是什么?是(x,y)还是(半径,角度) - Nawaz
@Nawaz,template<A1,A2>模式应该会有所帮助,对吧? OP可以传递任何(x,y)(radius,angle) - iammilind
2
你没有理解我的评论。OP想要传递笛卡尔坐标或者极坐标。两者恰好具有相同数量的参数和相同的隐式可转换类型。所以当你传递(1,2)时,它们是笛卡尔坐标还是极坐标?因为在每种情况下,point_t对象都将不同。 - Nawaz
@Nawaz,好的,我以为他会传递(1,2)(1.0f, 2.0f)。在这种情况下,我的逻辑应该是正确的。 - iammilind

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