วิธีการไม่อนุญาตชั่วคราว


107

สำหรับคลาส Foo มีวิธีที่จะไม่อนุญาตให้สร้างโดยไม่ตั้งชื่อได้หรือไม่?

ตัวอย่างเช่น:

Foo("hi");

และอนุญาตเฉพาะในกรณีที่คุณตั้งชื่อดังต่อไปนี้?

Foo my_foo("hi");

อายุการใช้งานของอันแรกเป็นเพียงคำสั่งและอันที่สองคือบล็อกปิดล้อม ในกรณีการใช้งานของฉันFooกำลังวัดเวลาระหว่างตัวสร้างและตัวทำลาย เนื่องจากฉันไม่เคยอ้างถึงตัวแปรภายในฉันจึงมักลืมใส่และเปลี่ยนอายุการใช้งานโดยไม่ได้ตั้งใจ ฉันต้องการรับข้อผิดพลาดเวลาคอมไพล์แทน


8
สิ่งนี้อาจมีประโยชน์สำหรับยามล็อค mutex
lucas clemente

1
คุณสามารถเขียนคอมไพเลอร์ C ++ ของคุณเองได้ในที่ที่ถูกห้าม แต่พูดอย่างเคร่งครัดว่ามันจะไม่ใช่ C ++ นอกจากนี้ยังมีสถานที่ที่จังหวะเช่นนั้นจะมีประโยชน์เช่นเมื่อส่งคืนวัตถุจากฟังก์ชันตัวอย่างเช่น (เช่นreturn std::string("Foo");)
โปรแกรมเมอร์บางคน

2
ไม่คุณทำไม่ได้ขอโทษ
Armen Tsirunyan

2
ขึ้นอยู่กับศาสนาของคุณนี่อาจเป็นกรณีที่มาโครอาจมีประโยชน์ (โดย usnig ประเภทที่เคยผ่านมาโครที่สร้างสิ่งที่แปรปรวนได้เสมอ)
PlasmaHH

3
ดูเหมือนว่าฉันต้องการให้เครื่องมือ LINT ของฉันจับได้มากกว่าสิ่งที่ฉันต้องการป้องกันด้วยการแฮ็คคอมไพเลอร์
Warren P

คำตอบ:


101

โซลูชันที่ใช้มาโครอื่น:

#define Foo class Foo

คำสั่งFoo("hi");ขยายไปถึงclass Foo("hi");ซึ่งผิดรูปแบบ; แต่Foo a("hi")ขยายเป็นclass Foo a("hi")ซึ่งถูกต้อง

สิ่งนี้มีข้อได้เปรียบที่เป็นทั้งซอร์สและไบนารีที่เข้ากันได้กับโค้ดที่มีอยู่ (ถูกต้อง) (การอ้างสิทธิ์นี้ไม่ถูกต้องทั้งหมดโปรดดูความคิดเห็นของ Johannes Schaub และการสนทนาที่ตามมาด้านล่าง: "คุณจะรู้ได้อย่างไรว่าเป็นซอร์สที่เข้ากันได้กับโค้ดที่มีอยู่เพื่อนของเขามีส่วนหัวของเขาและมีโมฆะ f () {int Foo = 0;} ซึ่งก่อนหน้านี้คอมไพล์ดีและคอมไพล์ผิด! นอกจากนี้ทุกบรรทัดที่กำหนดฟังก์ชันสมาชิกของคลาส Foo ล้มเหลว: โมฆะคลาส Foo :: bar () {} " )


51
คุณจะรู้ได้อย่างไรว่าเป็นซอร์สที่เข้ากันได้กับโค้ดที่มีอยู่ เพื่อนของเขามีส่วนหัวของเขาvoid f() { int Foo = 0; }ซึ่งก่อนหน้านี้รวบรวมได้ดีและตอนนี้คอมไพล์ผิด! void class Foo::bar() {}นอกจากนี้เส้นที่กำหนดฟังก์ชั่นสมาชิกของชั้นเรียนทุกฟูล้มเหลว:
Johannes Schaub - litb

21
ได้คะแนนโหวตมากมายขนาดนี้ได้ยังไง? เพียงแค่ดูความคิดเห็นโดย @ JohannesSchaub-litb แล้วคุณจะเข้าใจว่านี่เป็นทางออกที่แย่จริงๆ เนื่องจากคำจำกัดความทั้งหมดของฟังก์ชันสมาชิกไม่ถูกต้องหลังจากนี้ .. -1 จากด้านข้างของฉัน
Aamir

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

ฉันไม่แน่ใจว่าฉันเข้าใจ Foo ("hello") พยายามเรียก void Foo (float) และส่งผลให้เกิดข้อผิดพลาดของตัวเชื่อมโยงหรือไม่? แต่ทำไมเรียกรุ่นลูกลอยแทน Foo ctor?
ยกเลิก

2
undu หืมคุณใช้คอมไพเลอร์อะไร gcc 3.4 บ่นว่าไม่มีการแปลงเป็นลอย พยายามเรียกใช้ฟังก์ชันFooเนื่องจากมีความสำคัญเหนือชั้นเรียน

@aleguna ที่จริงฉันไม่ได้พยายามรันโค้ดนี้นั่นเป็นเพียงการเดา (ไม่ดี): s แต่คุณตอบคำถามของฉันอย่างไรก็ตามฉันไม่รู้ว่าฟังก์ชันมีความสำคัญเหนือชั้นเรียน
ยกเลิก

1
@didierc ไม่Foo::Foo("hi")ไม่อนุญาตใน C ++
Johannes Schaub - litb

44

กำหนดให้ตัวสร้างเป็นแบบส่วนตัว แต่ให้วิธีการสร้างแก่คลาส


9
-1: วิธีนี้ช่วยแก้ปัญหาของ OP ได้อย่างไร? คุณยังสามารถเขียนFoo::create();ทับได้Foo const & x = Foo::create();
Thomas Eding

@ThomasEding ฉันเดาว่าคุณพูดถูกมันไม่ได้แก้ไขปัญหาหลักของ OP แต่แค่บังคับให้เขาคิดและไม่ทำผิดที่เขากำลังทำ
dchhetri

1
@ThomasEding คุณไม่สามารถป้องกันตัวเองจากผู้ใช้ที่โกรธแค้นที่ต้องการทำลายระบบได้ แม้ว่าจะมีการแฮ็กของ @ ecatmur คุณสามารถพูดได้std::common_type<Foo>::type()และคุณได้รับชั่วคราว หรือแม้แต่typedef Foo bar; bar().
Johannes Schaub - litb

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

กรณีของf2, f3และการกลับมาของ"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;
}; 

ลักษณะสำหรับf2, f3และตอนนี้เสมอreturn "hello"// OK


2
Foo f = "hello"; // may throwแค่นี้ก็เพียงพอแล้วที่จะทำให้ฉันกลัวว่าจะไม่เคยใช้รหัสนี้
Thomas Eding

4
@thomas ฉันขอแนะนำให้ทำเครื่องหมายตัวสร้างexplicitจากนั้นรหัสดังกล่าวจะไม่รวบรวมอีกต่อไป เป้าหมายคือ fotbid ชั่วคราวและมันก็เป็นเช่นนั้น หากคุณกลัวคุณสามารถทำให้มันไม่โยนได้โดยตั้งค่าแหล่งที่มาของสำเนาในสำเนาหรือย้ายคอนสตรัคเตอร์ให้เป็นแบบไม่ชั่วคราว จากนั้นจะมีเพียงวัตถุสุดท้ายของสำเนาหลายชุดเท่านั้นหากยังคงเป็นเพียงชั่วคราว
Johannes Schaub - litb

2
พระเจ้า. ฉันไม่ใช่มือใหม่ใน C ++ และ C ++ 11 แต่ฉันไม่เข้าใจว่ามันทำงานอย่างไร คุณช่วยเพิ่มคำอธิบายได้ไหม ..
Mikhail

6
@ มิคาอิลลำดับการทำลายวัตถุชั่วคราวที่ถูกทำลายในจุดเดียวกันคือลำดับย้อนกลับของการก่อสร้าง อาร์กิวเมนต์ดีฟอลต์ที่ผู้โทรส่งผ่านเป็นการชั่วคราว หากFooอ็อบเจ็กต์เป็นแบบชั่วคราวเช่นกันและอายุการใช้งานสิ้นสุดลงในนิพจน์เดียวกันกับอาร์กิวเมนต์เริ่มต้นFoodtor ของอ็อบเจ็กต์จะถูกเรียกใช้ก่อน dtor ของอาร์กิวเมนต์เริ่มต้นเนื่องจากอดีตถูกสร้างขึ้นหลังจากหลัง
Johannes Schaub - litb

1
@ JohannesSchaub-litb เคล็ดลับที่ดีมาก ฉันคิดว่ามันเป็นไปไม่ได้ที่จะแยกแยะFoo(...);และFoo foo(...);จากภายในFoo.
Mikhail

18

ไม่กี่ปีที่ผ่านมาฉันได้เขียนโปรแกรมแก้ไขสำหรับคอมไพเลอร์ GNU C ++ ซึ่งเพิ่มตัวเลือกคำเตือนใหม่สำหรับสถานการณ์นั้น นี้จะติดตามในรายการ Bugzilla

น่าเสียดายที่ GCC Bugzilla เป็นสถานที่ฝังศพที่คำแนะนำคุณสมบัติที่รวมอยู่ในแพตช์ที่ได้รับการพิจารณาเป็นอย่างดีจะตาย :)

สิ่งนี้ได้รับแรงบันดาลใจจากความปรารถนาที่จะตรวจจับประเภทของจุดบกพร่องที่เป็นประเด็นของคำถามนี้ในรหัสซึ่งใช้วัตถุในพื้นที่เป็นอุปกรณ์สำหรับล็อคและปลดล็อกการวัดเวลาดำเนินการและอื่น ๆ


9

เช่นเดียวกับการใช้งานของคุณคุณไม่สามารถทำได้ แต่คุณสามารถใช้กฎนี้เพื่อประโยชน์ของคุณ:

อ็อบเจ็กต์ชั่วคราวไม่สามารถถูกผูกไว้กับการอ้างอิงที่ไม่ใช่ const

คุณสามารถย้ายรหัสจากคลาสไปยังฟังก์ชันอิสระซึ่งรับพารามิเตอร์อ้างอิงที่ไม่ใช่ const หากคุณทำเช่นนั้นคุณจะได้รับข้อผิดพลาดของคอมไพเลอร์หากมีการพยายามเชื่อมโยงกับการอ้างอิงที่ไม่ใช่ 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

@ 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

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

1
โอเคแก้ไขขีดล่าง @Griwes - อย่าเป็นพวกฝักใฝ่ฝ่ายใด ควรพูดว่า "ใช้มาโคร" ดีกว่า "ไม่สามารถทำได้"
amaurea

5
ก็ไม่สามารถทำได้ Foo();คุณยังไม่ได้แก้ปัญหาที่ทุกคนก็ยังคงสมบูรณ์ตามกฎหมายที่จะทำ
Puppy

11
ตอนนี้คุณกำลังดื้ออยู่ที่นี่ เปลี่ยนชื่อคลาส Foo เป็นสิ่งที่ซับซ้อนและเรียกมาโคร Foo แก้ไขปัญหา.
amaurea

8
สิ่งที่ชอบ:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley

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วิธีการที่คุณไม่สามารถลืมที่จะตั้งชื่อตัวแปรและคุณไม่สามารถลืมที่จะเขียน Verbose แต่ปลอดภัย


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 ++ Coding Standards, Item 40


3
fun(Foo("text"));นี้ไม่อนุญาตให้
Guilherme Bernal
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.