【问题标题】:How to disallow temporaries如何禁止临时工
【发布时间】:2012-10-31 14:00:38
【问题描述】:

对于 Foo 类,有没有办法在不给它命名的情况下禁止构造它?

例如:

Foo("hi");

只有在你给它一个名字的情况下才允许它,如下所示?

Foo my_foo("hi");

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

【问题讨论】:

  • 这对于互斥锁保护也很有用。
  • 好吧,你可以在被禁止的地方编写自己的 C++ 编译器,但严格来说,那时它不会是 C++。还有一些像这样的临时对象会很有用的地方,例如从函数返回对象时(例如return std::string("Foo");
  • 不,你不能这样做,对不起
  • 根据您的宗教信仰,这可能是宏可以派上用场的情况(通过使用该类型只能通过始终创建变量的宏)
  • 看起来更像是我希望我的 LINT 工具捕获的东西,而不是我想通过编译器 hack 在语法上阻止的东西。

标签: c++


【解决方案1】:

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

#define Foo class Foo

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

这具有与现有(正确)代码源代码和二进制兼容的优点。 (这种说法并不完全正确 - 请参阅 Johannes Schaub 的评论和下面的讨论:“你怎么知道它与现有代码的源代码兼容?他的朋友包括他的标题并且有 void f() { int Foo = 0; } 以前编译得很好,现在编译错误!此外,定义类 Foo 的成员函数的每一行都失败: void class Foo::bar() {}")

【讨论】:

  • 你怎么知道它与现有代码的源代码兼容?他的朋友包括他的标题和 void f() { int Foo = 0; },以前编译得很好,现在编译错误!此外,定义类 Foo 的成员函数的每一行都失败:void class Foo::bar() {}
  • 这怎么能得到这么多票?只要看看@JohannesSchaub-litb 的评论,你就会明白这是一个非常糟糕的解决方案。因为在此之后所有成员函数的定义都无效.. -1 从我这边
  • @JustMaximumPower:我希望这是讽刺的,因为如果不是,这又是一个糟糕的(读起来更糟)的解决方法。因为我们在取消定义后又回到了第一格,这意味着你不会在类似的行上得到编译错误(OP 的意图),即现在 Foo.cpp 内的Foo("Hi")
  • @Aamir 不,我是认真的。 Martin C. Martin 打算用它来保护 Foo 的使用而不是实现。
  • 我在Visual Studio 2012中试了一下,发现class Foo("hi");可以编译了。
【解决方案2】:

来个小技巧怎么样

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

void Foo (float);


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

【讨论】:

  • 伟大的黑客!注意:Foo a("hi");(没有class)也是一个错误。
  • 我不确定我是否理解。 Foo("hello") 尝试调用 void Foo(float) 并导致链接器错误?但是为什么要调用 float 版本而不是 Foo ctor 呢?
  • undu,嗯,你用的是什么编译器? gcc 3.4 抱怨没有转换为浮点数。它尝试调用函数Foo,因为它优先于类。
  • @aleguna 实际上我并没有尝试运行这段代码,那只是一个(错误的)猜测:s 但你还是回答了我的问题,我不知道函数优先于类。
  • @didierc no,Foo::Foo("hi") 在 C++ 中是不允许的。
【解决方案3】:

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

【讨论】:

  • -1:这如何解决 OP 的问题?你仍然可以在Foo const & x = Foo::create(); 上写Foo::create();
  • @ThomasEding 我猜你是对的,它并不能解决 OP 的核心问题,而只是迫使他思考而不是犯下他正在犯的错误。
  • @ThomasEding 您无法保护自己免受想要破坏系统的愤怒用户的侵害。即使使用@ecatmur 的hack,你也可以说std::common_type<Foo>::type(),你会得到一个临时的。甚至typedef Foo bar; bar().
  • @JohannesSchaub-litb:但最大的区别在于它是否是错误的。几乎没有办法输入std::common_type<Foo>::type() 是错误的。不小心漏掉Foo const & x = ... 是完全可信的。
【解决方案4】:

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

您想要保护的任何构造函数都需要一个默认参数,在该参数上调用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

【讨论】:

  • Foo f = "hello"; // may throw 这足以吓到我从不使用此代码。
  • @thomas,我建议将构造函数标记为explicit,然后不再编译此类代码。我们的目标是对临时投标进行投标,而且确实如此。如果您害怕,您可以通过将复制或移动构造函数中的副本源设置为非临时的来使其不会抛出。那么只有几个副本的最终对象如果仍然作为临时对象结束,则可能会抛出它。
  • 天哪。我不是 C++ 和 C++11 的新手,但我不明白这是如何工作的。您能否添加一些解释?..
  • @Mikhail 在同一点销毁的临时对象的销毁顺序是它们构造的相反顺序。调用者传递的默认参数是临时的。如果Foo 对象也是临时对象,并且它的生命周期以与默认参数相同的表达式结束,则Foo 对象的 dtor 将在默认参数的 dtor 之前被调用,因为前者是在后者之后创建的。
  • @JohannesSchaub-litb 非常好的技巧。我真的认为不可能从Foo 内部区分Foo(...);Foo foo(...);
【解决方案5】:

几年前,我为 GNU C++ 编译器写了a patch,它为这种情况添加了一个新的警告选项。这是在a Bugzilla item 中跟踪的。

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

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

【讨论】:

    【解决方案6】:

    在您的实现中,您不能这样做,但您可以利用此规则来发挥自己的优势:

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

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

    Code Sample

    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&)’
    

    【讨论】:

    • @didierc:如果他们提供了额外的功能。你可以不这样做。我们正在尝试调整一种方法来实现标准未明确允许的东西,所以当然会有限制.
    • @didierc 参数x 是一个命名对象,所以不清楚我们是否真的要禁止它。如果您使用的构造函数是显式的,可能人们会本能地使用Foo f = Foo("hello");。我想如果失败了,他们会生气。我的解决方案最初以异常/断言失败拒绝它(以及非常相似的情况),并且有人抱怨。
    • @JohannesSchaub-litb 是的,OP 希望通过强制绑定来禁止丢弃构造函数生成的值。我的例子是错误的。
    【解决方案7】:

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

    #include <iostream>
    using namespace std;
    
    enum SelfRef { selfRef };
    
    struct S
    {
        S( SelfRef, S const & ) {}
    };
    
    int main()
    {
        S a( selfRef, a );
    }
    

    【讨论】:

    • 好主意,但只要你有一个变量:S(selfRef, a);。 ://
    • @Xeo S(SelfRef, S const&amp; s) { assert(&amp;s == this); },如果运行时错误是可以接受的。
    【解决方案8】:

    不,恐怕这是不可能的。但是你可以通过创建一个宏来获得同样的效果。

    #define FOO(x) Foo _foo(x)
    

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

    【讨论】:

    • 我本来打算投票的,但后来我看到“你可以创建一个宏”。
    • 好的,修正了下划线。 @Griwes - 不要成为原教旨主义者。说“使用宏”比说“这不能做”要好。
    • 好吧,没办法。你根本没有解决问题,做Foo();还是完全合法的。
    • 现在你在这里很固执。将 Foo 类重命名为一些复杂的东西,然后调用宏 Foo。问题解决了。
    • 类似:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
    【解决方案9】:

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

    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。冗长,但安全。

    【讨论】:

      【解决方案10】:

      将单参数构造函数声明为显式的,没有人会无意中创建该类的对象。

      例如

      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 条。

      【讨论】:

      • 这确实允许fun(Foo("text"));
      猜你喜欢
      • 2019-07-28
      • 1970-01-01
      • 2010-09-10
      • 2017-02-15
      • 2011-06-03
      • 1970-01-01
      • 2017-08-07
      • 2019-07-26
      • 1970-01-01
      相关资源
      最近更新 更多