Bagaimana cara melarang pekerja sementara


107

Untuk kelas Foo, apakah ada cara untuk melarang pembuatannya tanpa memberinya nama?

Sebagai contoh:

Foo("hi");

Dan hanya izinkan jika Anda memberinya nama, seperti berikut?

Foo my_foo("hi");

Masa pakai yang pertama hanyalah pernyataan, dan yang kedua adalah blok penutup. Dalam kasus penggunaan saya, Foomengukur waktu antara konstruktor dan destruktor. Karena saya tidak pernah mengacu pada variabel lokal, saya sering lupa memasukkannya, dan tidak sengaja mengubah masa pakai. Saya ingin mendapatkan kesalahan waktu kompilasi.


8
Ini juga bisa berguna untuk penjaga kunci mutex.
lucas clemente

1
Nah, Anda bisa menulis compiler C ++ Anda sendiri di tempat yang dilarang, tetapi secara tegas itu bukan C ++. Ada juga tempat di mana temporer seperti itu akan berguna, seperti ketika mengembalikan objek dari suatu fungsi misalnya (seperti return std::string("Foo");)
Beberapa programmer bung

2
Tidak, Anda tidak bisa melakukan ini, maaf
Armen Tsirunyan

2
Bergantung pada agama Anda, ini mungkin kasus di mana makro bisa berguna (dengan menggunakan tipe itu hanya melalui makro yang selalu membuat
variabel

3
Tampaknya lebih seperti sesuatu yang saya ingin alat LINT saya tangkap daripada sesuatu yang saya ingin cegah secara sintaksis dengan peretasan kompiler.
Warren P

Jawaban:


101

Solusi berbasis makro lainnya:

#define Foo class Foo

Pernyataan itu Foo("hi");meluas ke class Foo("hi");, yang bentuknya buruk; tetapi Foo a("hi")berkembang menjadi class Foo a("hi"), yang benar.

Ini memiliki keuntungan karena kompatibel dengan sumber dan biner dengan kode yang ada (benar). (Klaim ini tidak sepenuhnya benar - silakan lihat Komentar Johannes Schaub dan diskusi selanjutnya di bawah ini: "Bagaimana Anda bisa tahu bahwa itu adalah sumber yang kompatibel dengan kode yang ada? Temannya menyertakan tajuknya dan memiliki void f () {int Foo = 0;} yang sebelumnya dikompilasi dengan baik dan sekarang salah dikompilasi! Selain itu, setiap baris yang mendefinisikan fungsi anggota kelas Foo gagal: void class Foo :: bar () {} " )


51
Bagaimana Anda tahu bahwa itu adalah sumber yang kompatibel dengan kode yang ada? Temannya memasukkan tajuknya dan void f() { int Foo = 0; }yang sebelumnya dikompilasi dengan baik dan sekarang salah kompilasi! Juga, setiap baris yang mendefinisikan fungsi anggota kelas Foo gagal: void class Foo::bar() {}.
Johannes Schaub - litb

21
Bagaimana ini bisa mendapatkan banyak suara? Lihat saja komentar oleh @ JohannesSchaub-litb dan Anda akan mengerti bahwa ini adalah solusi yang sangat buruk. Karena semua definisi fungsi anggota tidak valid setelah ini .. -1 dari sisi saya
Aamir

2
@JustMaximumPower: Saya harap itu sarkastik karena jika tidak, ini adalah solusi yang buruk (baca lebih buruk). Karena kita kembali ke titik awal setelah undefining itu, yang berarti bahwa Anda tidak akan mendapatkan kesalahan kompilasi (yang dimaksudkan OP) pada baris yang sama yaitu, Foo("Hi")di dalam Foo.cpp sekarang
Aamir

1
@Aamir Tidak, aku serius. Martin C. Martin bermaksud menggunakannya untuk menjaga penggunaan Foo, bukan implementasinya.
JustMaximumPower

1
Saya mencoba di Visual Studio 2012 dan ternyata class Foo("hi");OK untuk kompilasi.
fresky

71

Bagaimana dengan sedikit retasan

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

void Foo (float);


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

1
Retasan Hebat! Satu catatan: Foo a("hi");(tanpa class) akan menjadi kesalahan juga.
bitmask

Saya tidak yakin saya mengerti. Foo ("hello") mencoba memanggil void Foo (float) dan menghasilkan error linker? Tetapi mengapa versi float disebut sebagai pengganti Foo ctor?
undu

2
undu, hm compiler apa yang kamu gunakan? gcc 3.4 mengeluh bahwa tidak ada konversi ke float. Ia mencoba memanggil fungsi Fookarena ia didahulukan dari kelas.

@aleguna sebenarnya saya tidak mencoba menjalankan kode ini, itu hanya tebakan (buruk): s Tapi Anda tetap menjawab pertanyaan saya, saya tidak tahu fungsi lebih diutamakan daripada kelas.
undu

1
@didierc no, Foo::Foo("hi")tidak diizinkan di C ++.
Johannes Schaub - litb

44

Jadikan konstruktor pribadi tetapi berikan kelas metode buat .


9
-1: Bagaimana ini menyelesaikan masalah OP sama sekali? Anda masih dapat menulis Foo::create();lebihFoo const & x = Foo::create();
Thomas eding

@ThomasEding Saya kira Anda benar, itu tidak memperbaiki masalah inti OP, tetapi hanya memaksanya berpikir dan tidak membuat kesalahan yang dia buat.
dchhetri

1
@ThomasEding Anda tidak dapat melindungi diri Anda dari pengguna yang marah yang ingin merusak sistem. Bahkan dengan hack @ ecatmur Anda dapat mengatakan std::common_type<Foo>::type()dan Anda mendapatkan sementara. Atau bahkan typedef Foo bar; bar().
Johannes Schaub - litb

@ JohannesSchaub-litb: Tapi perbedaan besar adalah apakah itu karena kesalahan atau tidak. Hampir tidak mungkin mengetik std::common_type<Foo>::type()karena kesalahan. Meninggalkan Foo const & x = ...kecelakaan itu benar-benar bisa dipercaya.
Thomas Eding

24

Yang ini tidak menghasilkan kesalahan kompiler, tetapi kesalahan runtime. Alih-alih mengukur waktu yang salah, Anda mendapatkan pengecualian yang mungkin juga dapat diterima.

Setiap konstruktor yang ingin Anda jaga membutuhkan argumen default yang set(guard)dipanggil.

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

Ciri-cirinya adalah:

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

Kasus f2, f3dan kembalinya "hello"mungkin tidak diinginkan. Untuk mencegah pelemparan, Anda dapat mengizinkan sumber salinan menjadi sementara, dengan menyetel ulang guardmenjadi sekarang lindungi kami, bukan sumber salinan. Sekarang Anda juga melihat mengapa kami menggunakan petunjuk di atas - ini memungkinkan kami untuk menjadi fleksibel.

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

Ciri-ciri untuk f2, f3dan untuk return "hello"saat ini selalu // OK.


2
Foo f = "hello"; // may throwIni cukup untuk membuatku takut agar tidak pernah menggunakan kode ini.
Thomas Eding

4
@ Thomas, saya sarankan untuk menandai konstruktor explicitdan kemudian kode tersebut tidak lagi dikompilasi. tujuannya adalah untuk melayani yang sementara, dan itu berhasil. jika Anda takut, Anda dapat membuatnya tidak membuang dengan mengatur sumber salinan dalam salinan atau memindahkan konstruktor menjadi sementara. maka hanya objek akhir dari beberapa salinan yang dapat dibuang jika masih berakhir sebagai sementara.
Johannes Schaub - litb

2
Tuhanku. Saya bukan pemula di C ++ dan C ++ 11, tapi saya tidak mengerti bagaimana cara kerjanya. Bisakah Anda menambahkan beberapa penjelasan? ..
Mikhail

6
@Mikhail urutan penghancuran benda-benda sementara yang dihancurkan pada titik yang sama adalah urutan kebalikan dari konstruksi mereka. Argumen default yang diteruskan pemanggil bersifat sementara. Jika Fooobjek juga bersifat sementara, dan masa pakainya berakhir dengan ekspresi yang sama dengan argumen default, maka Foodtor objek akan dipanggil sebelum dtor argumen default, karena yang pertama dibuat setelah argumen default.
Johannes Schaub - litb

1
@ JohannesSchaub-litb Trik yang sangat bagus. Saya benar-benar berpikir tidak mungkin untuk membedakan Foo(...);dan Foo foo(...);dari dalam Foo.
Mikhail

18

Beberapa tahun yang lalu saya menulis tambalan untuk kompiler GNU C ++ yang menambahkan opsi peringatan baru untuk situasi itu. Ini dilacak dalam item Bugzilla .

Sayangnya, GCC Bugzilla adalah kuburan di mana saran fitur yang termasuk patch dianggap mati. :)

Hal ini dimotivasi oleh keinginan untuk menangkap dengan tepat jenis bug yang menjadi subjek pertanyaan ini dalam kode yang menggunakan objek lokal sebagai gadget untuk mengunci dan membuka kunci, mengukur waktu eksekusi dan sebagainya.


9

Apa adanya, dengan penerapan Anda, Anda tidak dapat melakukan ini, tetapi Anda dapat menggunakan aturan ini untuk keuntungan Anda:

Objek sementara tidak dapat terikat ke referensi non-const

Anda dapat memindahkan kode dari kelas ke fungsi berdiri bebas yang menggunakan parameter referensi non-const. Jika Anda melakukannya, Anda akan mendapatkan kesalahan compiler jika sementara mencoba untuk mengikat ke referensi non-const.

Contoh Kode

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

Keluaran

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: Asalkan mereka menyediakan fungsi tambahan Terserah Anda untuk tidak melakukannya Kami mencoba mengubah cara untuk mencapai sesuatu yang tidak secara eksplisit diizinkan oleh standar, jadi tentu saja akan ada batasan.
Alok Save

@didierc parameternya xadalah sebuah objek bernama jadi tidak jelas apakah kita benar-benar ingin melarangnya. Jika konstruktor yang Anda gunakan eksplisit, semoga orang secara naluriah melakukannya Foo f = Foo("hello");. Saya pikir mereka akan marah jika gagal. Solusi saya awalnya menolaknya (dan kasus yang sangat mirip) dengan pengecualian / pernyataan-kegagalan dan seseorang mengeluh.
Johannes Schaub - litb

@ JohannesSchaub-litb Ya, OP ingin melarang membuang nilai yang dihasilkan oleh konstruktor dengan memaksa binding. Contoh saya salah.
didierc

7

Cukup tidak memiliki konstruktor default, dan memang memerlukan referensi ke sebuah instance di setiap konstruktor.

#include <iostream>
using namespace std;

enum SelfRef { selfRef };

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

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

3
Nice idea, tapi begitu Anda memiliki satu variabel: S(selfRef, a);. : /
Xeo

3
@ Xeo S(SelfRef, S const& s) { assert(&s == this); }, jika kesalahan runtime dapat diterima.

6

Tidak, saya khawatir ini tidak mungkin. Tapi Anda bisa mendapatkan efek yang sama dengan membuat makro.

#define FOO(x) Foo _foo(x)

Dengan ini, Anda cukup menulis FOO (x) alih-alih Foo my_foo (x).


5
Saya akan memberi suara positif, tetapi kemudian saya melihat "Anda dapat membuat makro".
Griwes

1
Oke, perbaiki garis bawah. @Griwes - Jangan menjadi seorang fundamentalis. Lebih baik mengatakan "gunakan makro" daripada "ini tidak dapat dilakukan".
amaurea

5
Yah, itu tidak bisa dilakukan. Anda belum menyelesaikan masalah sama sekali, itu masih legal untuk dilakukan Foo();.
Anak anjing

11
Sekarang Anda keras kepala di sini. Ubah nama kelas Foo menjadi sesuatu yang rumit, dan panggil makro Foo. Masalah terpecahkan.
amaurea

8
Sesuatu seperti:class Do_not_use_this_class_directly_Only_use_it_via_the_FOO_macro;
Benjamin Lindley

4

Karena tujuan utamanya adalah mencegah bug, pertimbangkan ini:

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
}

Dengan begitu Anda tidak bisa lupa memberi nama variabel dan Anda tidak bisa lupa untuk menulis struct. Verbose, tapi aman.


1

Deklarasikan konstruktor satu parametrik sebagai eksplisit dan tidak ada yang akan membuat objek dari kelas itu secara tidak sengaja.

Sebagai contoh

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

void fun(const Foo&);

hanya dapat digunakan dengan cara ini

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

tapi tidak pernah seperti ini (melalui sementara di tumpukan)

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

Lihat juga: Alexandrescu, C ++ Coding Standards, Item 40.


3
Ini memungkinkan fun(Foo("text"));.
Guilherme Bernal
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.