如何禁止临时工


107

对于Foo类,有没有一种方法可以禁止在不给它命名的情况下构造它?

例如:

Foo("hi");

并且仅在给它起一个名字的情况下才允许它,如下所示?

Foo my_foo("hi");

第一个生命周期只是语句,第二个生命周期是封闭块。在我的用例中,Foo正在测量构造函数和析构函数之间的时间。由于我从不引用局部变量,因此我经常忘记放入局部变量,而无意中更改了生命周期。我想得到一个编译时错误。


8
这对于互斥锁防护也可能派上用场。
lucas clemente 2012年

1
好吧,您可以在禁止的地方编写自己的C ++编译器,但严格来说,那不是C ++。在某些地方,类似这样的临时用法也很有用,例如从函数返回对象时(例如return std::string("Foo");
某些程序员花了

2
不,您不能这样做,对不起
Armen Tsirunyan 2012年

2
根据您的宗教信仰,可能会遇到这样的情况:宏可能会派上用场(只有通过总是创建变量的宏才能使用此类型的usnig)
PlasmaHH 2012年

3
似乎比起我希望通过编译器黑客从语法上防止的东西,更像是我希望我的LINT工具能够抓住的东西。
沃伦·P

Answers:


101

另一个基于宏的解决方案:

#define Foo class Foo

该语句Foo("hi");扩展为class Foo("hi");,格式不正确;但Foo a("hi")扩展为class Foo a("hi"),这是正确的。

这样做的好处是,它与现有(正确的)代码在源和二进制方面都兼容。(此说法并不完全正确-请参阅Johannes Schaub的评论及随后的讨论:“您怎么知道它与现有代码在源代码上兼容?他的朋友包括他的标头,并且具有f(){int Foo = 0;}以前可以很好地编译,现在可以重新编译!而且,定义Foo类的成员函数的每一行都将失败:void class Foo :: bar(){}“


51
您怎么知道它与现有代码在源代码上兼容?他的朋友包括他的标头,void f() { int Foo = 0; }并且以前可以编译,现在可以错误编译了!此外,定义Foo类的成员函数的每一行都会失败:void class Foo::bar() {}
Johannes Schaub-litb 2012年

21
如何获得这么多的选票?只需查看@ JohannesSchaub-litb的评论,您就会知道这是一个非常糟糕的解决方案。因为此后成员函数的所有定义均无效
。.-

2
@JustMaximumPower:我希望这很讽刺,因为如果没有,那将是一个不好的解决方法。因为在取消定义后我们将回到平方,这意味着您不会在类似的行上看到编译错误(OP想要的),即Foo("Hi")在Foo.cpp内部
Aamir

1
@Aamir不,我是认真的。Martin C. Martin打算使用它来保护Foo的使用而非实现。
JustMaximumPower

1
我在Visual Studio 2012中尝试过,发现class Foo("hi");可以进行编译。
fresky

71

一点小技巧

class Foo
{
    public:
        Foo (const char*) {}
};

void Foo (float);


int main ()
{
    Foo ("hello"); // error
    class Foo a("hi"); // OK
    return 1;
}

1
很棒的哈克!请注意:(Foo a("hi");不带class)也将是一个错误。
bitmask 2012年

我不确定我是否理解。Foo(“ hello”)尝试调用void Foo(float)并导致链接器错误?但是,为什么调用float版本而不是Foo ctor?
Undu 2012年

2
恩杜,嗯,你在用什么编译器?gcc 3.4抱怨没有转换为float。它尝试调用一个函数,Foo因为它优先于类。

@aleguna实际上我没有尝试运行此代码,这只是一个(错误的)猜测:s但是无论如何,您都回答了我的问题,我不知道函数优先于类。
2012年

1
@didierc不,Foo::Foo("hi")在C ++中是不允许的。
约翰内斯·绍布

44

将构造函数设为私有,但为类提供一个create方法。


9
-1:这怎么解决OP的问题?你仍然可以写Foo::create();Foo const & x = Foo::create();
托马斯Eding

@ThomasEding我想你是对的,它不能解决OP的核心问题,而只是迫使他思考而不能犯他正在犯的错误。
dchhetri

1
@ThomasEding您无法保护自己免受想要破坏系统的愤怒用户的侵害。即使使用@ecatmur的骇客,您也可以说出来std::common_type<Foo>::type(),但您会得到一个临时权限。甚至typedef Foo bar; bar()
Johannes Schaub-litb 2012年

@ JohannesSchaub-litb:但是最大的区别在于它是否是错误的。几乎不可能std::common_type<Foo>::type()错误地输入。Foo const & x = ...完全忽略掉意外。
Thomas Eding

24

这不会导致编译器错误,但会导致运行时错误。您可以得到一个例外,而不是测量错误的时间,这也是可以接受的。

您要保护的任何构造函数都需要一个默认参数,该参数在其上set(guard)被调用。

struct Guard {
  Guard()
    :guardflagp()
  { }

  ~Guard() {
    assert(guardflagp && "Forgot to call guard?");
    *guardflagp = 0;
  }

  void *set(Guard const *&guardflag) {
    if(guardflagp) {
      *guardflagp = 0;
    }

    guardflagp = &guardflag;
    *guardflagp = this;
  }

private:
  Guard const **guardflagp;
};

class Foo {
public:
  Foo(const char *arg1, Guard &&g = Guard()) 
    :guard()
  { g.set(guard); }

  ~Foo() {
    assert(!guard && "A Foo object cannot be temporary!");
  }

private:
  mutable Guard const *guard;
}; 

特点是:

Foo f() {
  // OK (no temporary)
  Foo f1("hello");

  // may throw (may introduce a temporary on behalf of the compiler)
  Foo f2 = "hello";

  // may throw (introduces a temporary that may be optimized away
  Foo f3 = Foo("hello");

  // OK (no temporary)
  Foo f4{"hello"};

  // OK (no temporary)
  Foo f = { "hello" };

  // always throws
  Foo("hello");

  // OK (normal copy)
  return f;

  // may throw (may introduce a temporary on behalf of the compiler)
  return "hello";

  // OK (initialized temporary lives longer than its initializers)
  return { "hello" };
}

int main() {
  // OK (it's f that created the temporary in its body)
  f();

  // OK (normal copy)
  Foo g1(f());

  // OK (normal copy)
  Foo g2 = f();
}

的情况下f2f3并返回"hello"可能并不希望这样。为了防止抛出,您可以通过将重置guard为现在保护我们而不是副本的来源,从而使副本的来源成为临时来源。现在,您还将看到我们为什么使用上面的指针-它使我们变得灵活。

class Foo {
public:
  Foo(const char *arg1, Guard &&g = Guard()) 
    :guard()
  { g.set(guard); }

  Foo(Foo &&other)
    :guard(other.guard)
  {
    if(guard) {
      guard->set(guard);
    }
  }

  Foo(const Foo& other)
    :guard(other.guard)
  {
    if(guard) {
      guard->set(guard);
    }
  }

  ~Foo() {
    assert(!guard && "A Foo object cannot be temporary!");
  }

private:
  mutable Guard const *guard;
}; 

为特色f2f3return "hello"现在始终// OK


2
Foo f = "hello"; // may throw这足以吓到我永远不要使用此代码。
Thomas Eding

4
@thomas,我建议标记构造函数explicit,然后不再编译此类代码。我们的目标是禁止临时工,但确实如此。如果感到害怕,可以通过将副本的源设置为副本或将构造函数设置为非临时副本来使其不被抛出。那么如果最后一个对象仍然是临时对象,则只能抛出该对象的最后一个对象。
Johannes Schaub-litb 2012年

2
天哪。我不是C ++和C ++ 11的新手,但是我不明白这是如何工作的。?能否请你加一对夫妇解释...
米哈伊尔

6
@Mikhail在相同点处破坏的临时对象的破坏顺序与它们的构造相反。调用方传递的默认参数是一个临时参数。如果Foo对象也是临时对象,并且其生存期以与默认参数相同的表达式结尾,则将在默认参数Foodtor之前调用对象的dtor,因为前者是在后者之后创建的。
Johannes Schaub-litb 2012年

1
@ JohannesSchaub-litb非常好的技巧。我真的认为这是不可能区分Foo(...);Foo foo(...);从里面Foo
米哈伊尔(Mikhail)

18

几年前,我为GNU C ++编译器写了一个补丁,为这种情况添加了新的警告选项。在Bugzilla项中对此进行了跟踪。

不幸的是,GCC Bugzilla是一个埋葬的地方,经过深思熟虑的包含补丁的功能建议已死。:)

这是出于希望在代码中准确捕获属于该问题的错误的动机,该错误使用本地对象作为用于锁定和解锁,测量执行时间等的小工具。


9

照原样,在您的实现中,您不能执行此操作,但是可以使用此规则以发挥自己的优势:

临时对象不能绑定到非常量引用

您可以将代码从类移动到采用非常量引用参数的独立函数。如果这样做,则在临时尝试绑定到非const引用时会出现编译器错误。

代码样例

class Foo
{
    public:
        Foo(const char* ){}
        friend void InitMethod(Foo& obj);
};

void InitMethod(Foo& obj){}

int main()
{
    Foo myVar("InitMe");
    InitMethod(myVar);    //Works

    InitMethod("InitMe"); //Does not work  
    return 0;
}

输出量

prog.cpp: In function int main()’:
prog.cpp:13: error: invalid initialization of non-const reference of type Foo&’ from a temporary of type const char*’
prog.cpp:7: error: in passing argument 1 of void InitMethod(Foo&)’

1
@didierc:如果它们提供了附加功能,您可以不这样做,我们正在尝试调整一种方法来实现标准未明确允许的功能,因此当然会有限制。
Alok保存

@didierc参数x是一个命名对象,因此尚不清楚我们是否真的要禁止使用它。如果您将使用的构造函数是显式的,则人们可能会本能地这样做Foo f = Foo("hello");。我认为如果失败,他们会生气。我的解决方案最初拒绝了它(以及非常相似的案例),但出现异常/断言失败,有人投诉。
Johannes Schaub-litb 2012年

@ JohannesSchaub-litb是的,OP希望通过强制绑定来禁止丢弃构造函数生成的值。我的例子是错误的。
didierc

7

只是没有默认的构造函数,并且确实需要在每个构造函数中都引用一个实例。

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

struct S
{
    S( SelfRef, S const & ) {}
};

int main()
{
    S a( selfRef, a );
}

3
不错的主意,但是只要您有一个变量:S(selfRef, a);。:/
Xeo 2012年

3
@Xeo S(SelfRef, S const& s) { assert(&s == this); },如果可以接受运行时错误。

6

不,恐怕这是不可能的。但是您可以通过创建宏来获得相同的效果。

#define FOO(x) Foo _foo(x)

有了这个,您可以只写FOO(x)而不是Foo my_foo(x)。


5
我本来打算投票,但是后来我看到“您可以创建一个宏”。
Griwes 2012年

1
好,修复下划线。@Griwes-不要成为原教旨主义者。最好说“使用宏”而不是“不能完成”。
amaurea 2012年

5
好吧,那是不可能的。您根本没有解决问题,这样做仍然是完全合法的Foo();
小狗

11
现在你在这里很固执。将Foo类重命名为复杂的名称,然后调用宏Foo。问题解决了。
amaurea 2012年

8
像这样的东西:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
本杰明·林德利

4

由于主要目标是防止错误,请考虑以下事项:

struct Foo
{
  Foo( const char* ) { /* ... */ }
};

enum { Foo };

int main()
{
  struct Foo foo( "hi" ); // OK
  struct Foo( "hi" ); // fail
  Foo foo( "hi" ); // fail
  Foo( "hi" ); // fail
}

这样,您就不会忘记为变量命名,也不会忘记编写struct。详细,但安全。


1

将一个参数的构造函数声明为显式的,并且不会有人无意间创建该类的对象。

例如

class Foo
{
public: 
  explicit Foo(const char*);
};

void fun(const Foo&);

只能用这种方式

void g() {
  Foo a("text");
  fun(a);
}

但是永远不要这样(通过堆栈上的临时地址)

void g() {
  fun("text");
}

另请参阅:Alexandrescu,C ++编码标准,项目40。


3
这确实允许fun(Foo("text"));
Guilherme Bernal
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.