Làm thế nào để không cho phép thời gian tạm thời


107

Đối với một lớp Foo, có cách nào để không cho phép xây dựng nó mà không cần đặt tên cho nó không?

Ví dụ:

Foo("hi");

Và chỉ cho phép nó nếu bạn đặt cho nó một cái tên, như sau?

Foo my_foo("hi");

Vòng đời của cái đầu tiên chỉ là câu lệnh và cái thứ hai là khối bao quanh. Trong trường hợp sử dụng của tôi, Foolà đo thời gian giữa hàm tạo và hàm hủy. Vì tôi không bao giờ tham chiếu đến biến cục bộ, tôi thường quên đưa nó vào và vô tình thay đổi thời gian tồn tại. Thay vào đó, tôi muốn gặp lỗi thời gian biên dịch.


8
Điều này cũng có thể hữu ích cho bảo vệ khóa mutex.
lucas clemente

1
Chà, bạn có thể viết trình biên dịch C ++ của riêng mình khi nó bị cấm, nhưng nói đúng ra thì nó sẽ không phải là C ++. Ngoài ra còn có những nơi là tạm thời như vậy sẽ có ích, giống như khi trả lại một đối tượng từ một chức năng ví dụ (như return std::string("Foo");)
Một số lập trình viên anh chàng

2
Không, bạn không thể làm được điều này, xin lỗi
Armen Tsirunyan

2
Tùy thuộc vào tôn giáo của bạn này có thể là một trường hợp macro có thể đến tiện dụng (bằng cách usnig kiểu đó chỉ từng qua một macro mà luôn tạo ra một varaible)
PlasmaHH

3
Có vẻ như thứ gì đó mà tôi muốn công cụ LINT của mình nắm bắt được hơn là thứ mà tôi muốn ngăn chặn về mặt cú pháp bằng cách hack trình biên dịch.
Warren P,

Câu trả lời:


101

Một giải pháp dựa trên vĩ mô khác:

#define Foo class Foo

Câu lệnh Foo("hi");mở rộng class Foo("hi");thành, không đúng; nhưng Foo a("hi")mở rộng thành class Foo a("hi"), đó là chính xác.

Điều này có lợi thế là nó tương thích với cả mã nguồn và mã nhị phân với mã hiện có (đúng). (Tuyên bố này không hoàn toàn đúng - vui lòng xem Bình luận của Johannes Schaub và cuộc thảo luận tiếp theo bên dưới: "Làm thế nào bạn có thể biết rằng nó là nguồn tương thích với mã hiện có? Bạn của anh ấy bao gồm tiêu đề của anh ấy và có void f () {int Foo = 0;} mà trước đây biên dịch tốt và bây giờ biên dịch sai! Ngoài ra, mọi dòng xác định hàm thành viên của lớp Foo đều bị lỗi: void class Foo :: bar () {} " )


51
Làm thế nào bạn có thể biết rằng nó là nguồn tương thích với mã hiện có? Bạn của anh ấy bao gồm tiêu đề của anh ấy và void f() { int Foo = 0; }trước đây đã biên dịch tốt và bây giờ biên dịch sai! Ngoài ra, tất cả các dòng định nghĩa một hàm thành viên của lớp Foo thất bại: void class Foo::bar() {}.
Johannes Schaub - litb

21
Làm thế nào mà điều này có thể nhận được nhiều phiếu bầu? Chỉ cần nhìn vào bình luận của @ JohannesSchaub-litb và bạn sẽ hiểu rằng đây là một giải pháp thực sự tồi. Bởi vì tất cả các định nghĩa của các hàm thành viên không hợp lệ sau này .. -1 từ bên cạnh tôi
Aamir

2
@JustMaximumPower: Tôi hy vọng điều đó là mỉa mai vì nếu không, nó lại là một cách giải quyết tồi (đọc tệ hơn). Bởi vì chúng tôi đang trở lại một hình vuông sau undefining nó, mà có nghĩa là bạn sẽ không nhận được một lỗi biên dịch (mà OP dự định) trên một dòng ví dụ tương tự, Foo("Hi")bên trong Foo.cpp nay
Aamir

1
@Aamir Không, tôi nói thật đấy. Martin C. Martin có ý định sử dụng nó để bảo vệ việc sử dụng Foo không được thực hiện.
JustMaximumPower

1
Tôi đã thử trong Visual Studio 2012 và thấy rằng class Foo("hi");biên dịch là OK.
tươi mát

71

Làm thế nào về một hack nhỏ

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

void Foo (float);


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

1
Hack tuyệt vời! Một lưu ý: Foo a("hi");(không có class) cũng sẽ là một lỗi.
bitmask,

Tôi không chắc là tôi hiểu. Foo ("xin chào") cố gắng gọi void Foo (float) và nó dẫn đến lỗi trình liên kết? Nhưng tại sao phiên bản float lại được gọi thay vì Foo ctor?
undu,

2
undu, hm bạn đang sử dụng trình biên dịch nào? gcc 3.4 phàn nàn rằng không có chuyển đổi thành float. Nó cố gắng gọi một hàm Foovì nó được ưu tiên hơn một lớp.

@aleguna thực sự thì tôi không cố chạy mã này, đó chỉ là một phỏng đoán (tệ): s Nhưng dù sao thì bạn cũng đã trả lời câu hỏi của tôi rồi, tôi không biết hàm được ưu tiên hơn lớp.
undu,

1
@didierc không, không Foo::Foo("hi")được phép trong C ++.
Johannes Schaub - litb

44

Đặt hàm tạo riêng tư nhưng cung cấp cho lớp một phương thức tạo .


9
-1: Điều này giải quyết vấn đề của OP như thế nào? Bạn vẫn có thể viết Foo::create();trênFoo const & x = Foo::create();
Thomas Việc chỉnh sửa

@ThomasEding Tôi đoán bạn nói đúng, nó không khắc phục được vấn đề cốt lõi của OP mà chỉ buộc anh ấy suy nghĩ và không mắc sai lầm mà anh ấy đang mắc phải.
dchhetri 31/10/12

1
@ThomasEding bạn không thể tự bảo vệ mình trước những người dùng tức giận muốn phá vỡ hệ thống. Ngay cả với vụ hack của @ ecatmur, bạn có thể nói std::common_type<Foo>::type()và tạm thời. Hoặc thậm chí typedef Foo bar; bar().
Johannes Schaub - litb

@ JohannesSchaub-litb: Nhưng sự khác biệt lớn là có phải do nhầm lẫn hay không. Hầu như không có chuyện gõ std::common_type<Foo>::type()nhầm. Việc bỏ qua một Foo const & x = ...cách tình cờ là hoàn toàn có thể tin được.
Thomas Eding

24

Điều này không dẫn đến lỗi trình biên dịch mà là lỗi thời gian chạy. Thay vì đo sai thời gian, bạn nhận được một ngoại lệ có thể chấp nhận được.

Bất kỳ hàm tạo nào bạn muốn bảo vệ đều cần một đối số mặc định set(guard)được gọi.

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;
}; 

Các đặc điểm là:

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

Trường hợp của f2, f3và việc trả lại "hello"có thể không được mong muốn. Để tránh bị ném, bạn có thể cho phép nguồn của một bản sao chỉ là nguồn tạm thời, bằng cách đặt lại nguồn guardđể bảo vệ chúng tôi ngay bây giờ thay vì nguồn của bản sao. Bây giờ bạn cũng thấy lý do tại sao chúng tôi sử dụng các con trỏ ở trên - nó cho phép chúng tôi linh hoạt.

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;
}; 

Các đặc tính cho f2, f3và cho return "hello"bây giờ luôn // OK.


2
Foo f = "hello"; // may throwĐiều này đủ để khiến tôi sợ rằng không bao giờ sử dụng mã này.
Thomas Eding

4
@thomas, tôi khuyên bạn nên đánh dấu hàm tạo explicitvà sau đó mã như vậy không cần biên dịch nữa. mục tiêu là để trả giá tạm thời, và nó có. nếu bạn sợ hãi, bạn có thể làm cho nó không bị ném bằng cách đặt nguồn của một bản sao trong phương thức khởi tạo sao chép hoặc di chuyển là không tạm thời. thì chỉ đối tượng cuối cùng của một số bản sao mới có thể ném nếu nó vẫn là đối tượng tạm thời.
Johannes Schaub - litb

2
Chúa tôi. Tôi không phải là người mới làm quen với C ++ và C ++ 11, nhưng tôi không thể hiểu cách này hoạt động. Bạn có thể vui lòng thêm một vài lời giải thích? ..
Mikhail

6
@Mikhail thứ tự phá hủy của các đối tượng tạm thời bị phá hủy tại các điểm giống nhau là thứ tự ngược lại của việc xây dựng chúng. Đối số mặc định mà người gọi chuyển qua là đối số tạm thời. Nếu Foođối tượng cũng là đối tượng tạm thời và thời gian tồn tại của nó kết thúc trong cùng một biểu thức như đối số mặc định, thì Foodtor của đối tượng sẽ được gọi trước dtor của đối số mặc định, vì đối tượng trước được tạo sau đối số sau.
Johannes Schaub - litb

1
@ JohannesSchaub-litb Thủ thuật rất hay. Tôi thực sự nghĩ rằng nó là không thể phân biệt Foo(...);Foo foo(...);từ bên trong Foo.
Mikhail

18

Một vài năm trước, tôi đã viết một bản vá cho trình biên dịch GNU C ++ bổ sung một tùy chọn cảnh báo mới cho tình huống đó. Điều này được theo dõi trong một mục Bugzilla .

Thật không may, GCC Bugzilla là một hố chôn nơi các đề xuất tính năng bao gồm bản vá được coi là tốt sẽ chết. :)

Điều này được thúc đẩy bởi mong muốn bắt chính xác loại lỗi là chủ đề của câu hỏi này trong mã sử dụng các đối tượng cục bộ làm tiện ích để khóa và mở khóa, đo thời gian thực thi, v.v.


9

Tương tự, với việc triển khai của bạn, bạn không thể làm điều này, nhưng bạn có thể sử dụng quy tắc này để có lợi cho mình:

Các đối tượng tạm thời không thể bị ràng buộc với các tham chiếu không phải const

Bạn có thể di chuyển mã từ lớp sang một hàm tự do nhận tham số tham chiếu không phải const. Nếu bạn làm như vậy, Bạn sẽ gặp lỗi trình biên dịch nếu tạm thời cố gắng liên kết với tham chiếu không phải const.

Mẫu mã

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;
}

Đầu ra

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: Miễn là họ cung cấp một chức năng bổ sung. Bạn không nên làm như vậy. Chúng tôi đang cố gắng điều chỉnh một cách để đạt được điều gì đó không được tiêu chuẩn cho phép một cách rõ ràng, vì vậy tất nhiên sẽ có những hạn chế.
Alok Save

@didierc tham số xlà một đối tượng được đặt tên nên không rõ chúng ta có thực sự muốn cấm nó hay không. Nếu hàm tạo mà bạn đã sử dụng là rõ ràng, mọi người có thể làm theo bản năng Foo f = Foo("hello");. Tôi nghĩ họ sẽ tức giận nếu nó thất bại. Giải pháp của tôi ban đầu đã từ chối nó (và những trường hợp rất tương tự) với một ngoại lệ / khẳng định-thất bại và ai đó đã phàn nàn.
Johannes Schaub - litb

@ JohannesSchaub-litb Có, OP muốn cấm loại bỏ giá trị được tạo bởi một hàm tạo bằng cách buộc các ràng buộc. Ví dụ của tôi là sai.
didierc

7

Đơn giản là không có một hàm tạo mặc định và yêu cầu tham chiếu đến một thể hiện trong mọi hàm tạo.

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

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

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

3
Ý tưởng tốt đẹp, nhưng ngay sau khi bạn có một biến: S(selfRef, a);. : /
Xeo

3
@Xeo S(SelfRef, S const& s) { assert(&s == this); }, nếu lỗi thời gian chạy có thể chấp nhận được.

6

Không, tôi e rằng điều này là không thể. Nhưng bạn có thể nhận được hiệu ứng tương tự bằng cách tạo macro.

#define FOO(x) Foo _foo(x)

Với điều này, bạn chỉ có thể viết FOO (x) thay vì Foo my_foo (x).


5
Tôi đã định ủng hộ, nhưng sau đó tôi thấy "bạn có thể tạo macro".
Griwes 31/10/12

1
Ok, đã sửa các dấu gạch dưới. @Griwes - Đừng là người theo chủ nghĩa chính thống. Tốt hơn là nói "sử dụng macro" hơn là "điều này không thể được thực hiện".
amaurea,

5
Vâng, nó không thể được thực hiện. Bạn vẫn chưa giải quyết được vấn đề, việc này vẫn hoàn toàn hợp pháp Foo();.
Puppy

11
Bây giờ bạn đang cứng đầu ở đây. Đổi tên lớp Foo một cái gì đó phức tạp và gọi macro Foo. Vấn đề đã được giải quyết.
amaurea,

8
Một cái gì đó như:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley

4

Vì mục tiêu chính là ngăn chặn lỗi, hãy xem xét điều này:

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
}

Bằng cách đó, bạn không thể quên đặt tên cho biến và bạn không thể quên viết struct. Dài dòng, nhưng an toàn.


1

Khai báo hàm tạo một tham số là rõ ràng và không ai sẽ vô tình tạo một đối tượng của lớp đó.

Ví dụ

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

void fun(const Foo&);

chỉ có thể được sử dụng theo cách này

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

nhưng không bao giờ theo cách này (thông qua một tạm thời trên ngăn xếp)

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

Xem thêm: Alexandrescu, Tiêu chuẩn mã hóa C ++, Mục 40.


3
Điều này cho phép fun(Foo("text"));.
Guilherme Bernal
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.