Як заборонити тимчасово


107

Для класу Foo, чи існує спосіб заборонити його конструювання, не даючи ім'я?

Наприклад:

Foo("hi");

І дозволити це лише в тому випадку, якщо ви дасте ім’я, як-от наступне?

Foo my_foo("hi");

Час життя першого - це лише твердження, а другого - блок, що додається. У моєму випадку використання - Fooце вимірювання часу між конструктором та деструктором. Оскільки я ніколи не посилаюсь на локальну змінну, я часто забуваю помістити її і випадково змінити час життя. Я хотів би отримати помилку часу компіляції.


8
Це також може стати в нагоді для охоронців замкових файлів.
lucas clemente

1
Ну, ви можете написати свій власний компілятор C ++ там, де це було заборонено, але строго кажучи, тоді це не було б C ++. Є також місця, де подібні часописи були б корисні, як, наприклад, при поверненні об'єкта з функції, наприклад (наприклад return std::string("Foo");)
Деякі програміст чувак

2
Ні, ви не можете цього зробити, вибачте
Армен Цирунян

2
Залежно від вашої релігії, це може бути корисним випадком, коли макроси можуть стати в нагоді (використовуючи цей режим тільки коли-небудь через макрос, який завжди створює
змінну

3
Схоже, щось, що я хотів би, щоб мій інструмент LINT зафіксував, ніж щось, що я хотів би синтаксично запобігти злому компілятора.
Warren P

Відповіді:


101

Ще одне рішення на основі макросу:

#define Foo class Foo

Заява Foo("hi");розширюється до того class Foo("hi");, що неправильно формується; але Foo a("hi")розширюється на class Foo a("hi"), що правильно.

Це має перевагу в тому, що він є одночасно вихідним та бінарним сумісним із існуючим (правильним) кодом. (Ця претензія не зовсім коректна - дивіться коментар Йоганнеса Шауба та дискусію, що випливає нижче: "Як ви можете знати, що це джерело, сумісне з наявним кодом? який раніше компілював штрафи, а тепер неправильно компілюється! Також кожен рядок, що визначає функцію члена класу Foo, не вдається: void class Foo :: bar () {} " )


51
Як ви можете знати, що це джерело, сумісне з існуючим кодом? Його друг включає його заголовок і void f() { int Foo = 0; }який раніше склав штрафи, а тепер неправильно компілюється! Крім того , кожен рядок , яка визначає функцію - член класу Foo зазнає поразки: void class Foo::bar() {}.
Йоханнес Шауб - ліб

21
Як за це можна отримати стільки голосів? Просто подивіться на коментар @ JohannesSchaub-litb, і ви зрозумієте, що це дійсно погане рішення. Тому що всі визначення функцій-членів після цього недійсні .. -1 з мого боку
Аамір

2
@JustMaximumPower: Я сподіваюся, що це було саркастично, тому що якщо ні, це знову поганий (читати гірше) спосіб вирішення. Тому що ми повертаємося до квадратного після визначення його, а це означає, що ви не отримаєте помилку компіляції (яку призначив ОП) на подібній лінії, тобто Foo("Hi")всередині Foo.cpp зараз
Aamir

1
@Aamir Ні, я серйозно. Мартін К. Мартін має намір використовувати його для захисту використання Foo, а не реалізації.
JustMaximumPower

1
Я спробував у Visual Studio 2012 і виявив, що class Foo("hi");це добре для компіляції.
жартівливий

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) теж буде помилкою.
бітмаска

Я не впевнений, що розумію. Foo ("привіт") намагається викликати недійсний Foo (float), і це призводить до помилки лінкера? Але чому називається плаваюча версія замість Foo ctor?
неділя

2
undu, хм який компілятор ви використовуєте? gcc 3.4 скаржиться, що перетворення в плаваючий не існує. Він намагається викликати функцію, Fooоскільки вона має перевагу над класом.

@aleguna насправді я не намагався запустити цей код, це була лише (погана) здогадка: s Але ти все одно відповів на моє запитання, я не знав, що функція має перевагу над класом.
неділю

1
@didierc ні, Foo::Foo("hi")заборонено в C ++.
Йоханнес Шауб - ліб

44

Зробіть конструктор приватним, але дайте класу метод створення .


9
-1: Як це взагалі вирішує проблему ОП? Ви все ще можете написати Foo::create();більшFoo const & x = Foo::create();
Thomas Eding

@ThomasEding Я думаю, ви праві, це не вирішує основної проблеми ОП, а просто змушує його думати і не робити помилку, яку він робить.
дхетрі

1
@ThomasEding ви не можете захистити себе від розлючених користувачів, які хочуть зламати систему. Навіть з хаком @ ecatmur ви можете сказати, std::common_type<Foo>::type()і ви отримаєте тимчасовий характер. Або навіть typedef Foo bar; bar().
Йоханнес Шауб - ліб

@ JohannesSchaub-litb: Але велика різниця в тому, чи це було помилкою чи ні. Майже немає способу ввести std::common_type<Foo>::type()помилково. Покинути Foo const & x = ...випадково випадково цілком правдоподібно.
Томас Едінг

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Цього достатньо, щоб налякати мене ніколи не використовувати цей код.
Томас Едінг

4
@thomas, я рекомендую позначити конструктор explicitі тоді такий код більше не компілюється. мета полягала у забороні тимчасового, і це робиться. якщо ви боїтеся, ви можете змусити його не кидати, встановивши джерело копії в копії або перемістивши конструктор як нечасний. то лише кінцевий об'єкт з кількох копій може кинути, якщо він все-таки закінчується як тимчасовий.
Йоханнес Шауб - ліб

2
Боже. Я не новачок у C ++ та C ++ 11, але не можу зрозуміти, як це працює. Не могли б ви додати пару пояснень? ..
Михайло

6
@Mikhail порядок знищення тимчасових об'єктів, які знищуються в тих же точках, є зворотним порядком їх побудови. Аргумент за замовчуванням, який абонент передає, є тимчасовим. Якщо Fooоб’єкт також є тимчасовим, і його термін експлуатації закінчується тим же виразом, що і аргумент за замовчуванням, тоді Foodtor об'єкта буде викликаний перед dtor аргументу за замовчуванням, оскільки перший був створений після останнього.
Йоханнес Шауб - ліб

1
@ JohannesSchaub-litb Дуже приємний трюк. Я дійсно вважав, що неможливо розрізнити Foo(...);і Foo foo(...);зсередини Foo.
Михайло

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");. Думаю, вони зляться, якщо це не вдасться. Моє рішення спочатку відхиляло його (і дуже подібні випадки) за винятком / ствердження-відмови, і хтось скаржився.
Йоханнес Шауб - ліб

@ JohannesSchaub-litb Так, ОП хоче заборонити відкидати значення, згенеровані конструктором, примушуючи прив’язки. Мій приклад неправильний.
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);. : /
Ксео

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();.
Щеня

11
Тепер ви тут вперті. Перейменуйте клас Foo чимось складним і зателефонуйте на макрос Foo. Проблема вирішена.
amaurea

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

Дивіться також: Олександреску, Стандарти кодування C ++, пункт 40.


3
Це дійсно дозволяє fun(Foo("text"));.
Гільгерме Бернал
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.