Как запретить временные


107

Есть ли способ запретить создание класса Foo без указания имени?

Например:

Foo("hi");

И разрешить его, только если вы дадите ему имя, например, следующее?

Foo my_foo("hi");

Время жизни первого - это просто инструкция, а второго - закрывающий блок. В моем случае Fooизмерения время между конструктором и деструктором. Поскольку я никогда не ссылаюсь на локальную переменную, я часто забываю ее вставить и случайно меняю время жизни. Вместо этого я хотел бы получить ошибку времени компиляции.


8
Это также может пригодиться для защиты мьютексов.
Лукас клементе

1
Что ж, вы могли бы написать свой собственный компилятор C ++ там, где это запрещено, но, строго говоря, тогда это был бы не C ++. Есть также места, где подобные временные файлы могут быть полезны, например, при возврате объекта из функции (например return std::string("Foo");)
чувак-программист

2
Нет, вы не можете этого сделать, извините
Армен Цирунян

2
В зависимости от вашей религии это может быть тот случай, когда макросы могут пригодиться (по usnig этот тип только когда-либо через макрос, который всегда создает переменную)
PlasmaHH

3
Это больше похоже на то, что я бы хотел, чтобы мой инструмент LINT уловил, чем на то, что я хотел бы синтаксически предотвратить с помощью взлома компилятора.
Уоррен П.

Ответы:


101

Еще одно решение на основе макросов:

#define Foo class Foo

Утверждение Foo("hi");расширяется до class Foo("hi");неправильного формата; но Foo a("hi")расширяется до class Foo a("hi"), что правильно.

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


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

21
Как за это набрать столько голосов? Просто посмотрите комментарий @ JohannesSchaub-litb, и вы поймете, что это действительно плохое решение. Потому что все определения функций-членов после этого недействительны .. -1 с моей стороны
Аамир

2
@JustMaximumPower: Надеюсь, это было саркастично, потому что в противном случае это снова плохой (читай хуже) обходной путь. Потому что мы вернулись к исходной точке после отмены его определения, а это означает, что вы не получите ошибку компиляции (которая предназначалась OP) в аналогичной строке, то есть Foo("Hi")внутри Foo.cpp сейчас
Аамир

1
@ Аамир Нет, я серьезно. Мартин С. Мартин намеревается использовать его для защиты использования Foo, а не реализации.
JustMaximumPower 02

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
унду, хм какой компилятор ты используешь? gcc 3.4 жалуется, что нет преобразования в float. Он пытается вызвать функцию, Fooпотому что она имеет приоритет над классом.

@aleguna на самом деле я не пробовал запускать этот код, это была просто (плохая) догадка: s Но вы все равно ответили на мой вопрос, я не знал, что функция имеет приоритет над классом.
Унду

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

44

Сделайте конструктор закрытым, но дайте классу метод create .


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().
Йоханнес Шауб - лит

@ 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значение now 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и тогда такой код больше не компилируется. Целью было сделать ставку на временное, и это действительно так. если вы напуганы, вы можете сделать так, чтобы он не генерировался, установив источник копии в конструкторе копирования или перемещения как невременный. тогда только последний объект из нескольких копий может бросить, если он все еще остается временным.
Йоханнес Шауб - лит

2
О Господи. Я не новичок в C ++ и C ++ 11, но не могу понять, как это работает. Не могли бы вы добавить пару пояснений? ..
Михаил

6
@Mikhail порядок уничтожения временных объектов, которые уничтожаются в одних и тех же точках, обратный порядку их построения. Аргумент по умолчанию, который передает вызывающий, является временным. Если Fooобъект также является временным и его время жизни заканчивается тем же выражением, что и аргумент по умолчанию, то Foodtor объекта будет вызываться перед dtor аргумента по умолчанию, потому что первый был создан после последнего.
Йоханнес Шауб - лит

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

18

Несколько лет назад я написал патч для компилятора GNU C ++, который добавляет новый параметр предупреждения для этой ситуации. Это отслеживается в элементе Bugzilla .

К сожалению, GCC Bugzilla - это кладбище, где умирают хорошо продуманные предложения по включенным исправлениям. :)

Это было мотивировано желанием отловить именно те ошибки, которые являются предметом этого вопроса, в коде, который использует локальные объекты в качестве гаджетов для блокировки и разблокировки, измерения времени выполнения и т. Д.


9

Как и в вашей реализации, вы не можете этого сделать, но можете использовать это правило в своих интересах:

Временные объекты не могут быть привязаны к неконстантным ссылкам

Вы можете переместить код из класса в автономную функцию, которая принимает неконстантный ссылочный параметр. Если вы это сделаете, вы получите ошибку компилятора, если временное приложение попытается привязаться к неконстантной ссылке.

Образец кода

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 Save

@didierc параметр xявляется именованным объектом, поэтому неясно, действительно ли мы хотим его запретить. Если бы конструктор, который вы бы использовали, явный, люди могут инстинктивно это сделать Foo f = Foo("hello");. Я думаю, они бы разозлились, если бы это не удалось. Мое решение изначально отклонило его (и очень похожие случаи) с исключением / отказом утверждения, и кто-то пожаловался.
Йоханнес Шауб - лит

@ 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();.
Щенок

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"));.
Guilherme Bernal
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.