(Ab) использование конструкторов и деструкторов для побочных эффектов плохой практики? Альтернативы? - PullRequest
21 голосов
/ 22 июля 2010

В OpenGL часто пишут такой код:

glPushMatrix();
// modify the current matrix and use it
glPopMatrix();

По существу, состояние изменяется, затем выполняются некоторые действия, использующие новое состояние, и, наконец, состояние восстанавливается.

Теперь есть две проблемы:

  1. Легко забыть восстановить состояние.
  2. Если промежуточный код выдает исключение, состояние никогда не восстанавливается.

В истинном объектно-ориентированном стиле программирования я написал несколько служебных классов для решения этих проблем, например:

struct WithPushedMatrix {
    WithPushedMatrix() { glPushMatrix(); }
    ~WithPushedMatrix() { glPopMatrix(); }
};

Теперь я могу просто написать свой предыдущий пример следующим образом:

WithPushedMatrix p;
// modify the current matrix and use it

Точный момент восстановления определяется временем жизни p.Если выдается исключение, вызывается деструктор p, восстанавливается состояние, и жизнь хороша.

Тем не менее, я не совсем счастлив.Особенно, если конструктор принимает некоторые аргументы (например, флаги для glEnable), легко забыть присвоить объект переменной:

WithEnabledFlags(GL_BLEND); // whoops!

Временное уничтожается немедленно, а изменение состояния преждевременно отменяется..

Другая проблема заключается в том, что любой, кто читает мой код, может запутаться: «Почему объявленная здесь переменная никогда не используется? Давайте избавимся от нее!»

Итак, мои вопросы: Это хороший шаблон?Может быть, у него есть имя?Есть ли проблемы с этим подходом, который я пропускаю?И последнее, но не менее важное: есть ли хорошие альтернативы?

Обновление : Да, я думаю, это форма RAII.Но не так, как обычно используется RAII, потому что он включает в себя, казалось бы, бесполезную переменную;рассматриваемый «ресурс» никогда не доступен явно.Я просто не осознавал, что это конкретное использование было настолько распространенным.

Ответы [ 10 ]

24 голосов
/ 22 июля 2010

Мне нравится идея использования RAII для управления состоянием OpenGL, но на самом деле я бы сделал еще один шаг вперед: пусть ваш конструктор класса WithFoo принимает указатель на функцию в качестве параметра, который содержит код, который вы хотите выполнить в этот контекст. Затем не создайте именованные переменные, а просто работайте с временными файлами, передавая действие, которое вы хотите выполнить в этом контексте, как лямбду. (конечно, нужен C ++ 0x - может работать и с обычными указателями на функции, но это не так красиво).
Примерно так: (отредактировано для восстановления исключительной безопасности)

class WithPushedMatrix
{
public:
    WithPushedMatrix()
    {
        glPushMatrix();
    }

    ~WithPushedMatrix()
    {
        glPopMatrix();
    }

    template <typename Func>
    void Execute(Func action)
    {
        action();
    }
};

И используйте это так:

WithPushedMatrix().Execute([]
{
    glBegin(GL_LINES);
    //etc. etc.
});

Временный объект установит ваше состояние, выполнит действие, а затем автоматически разрушит его; у вас нет «свободных» переменных состояния, плавающих вокруг, и действия, выполняемые в контексте, становятся сильно связанными с ним. Вы даже можете вкладывать несколько контекстных действий, не беспокоясь о порядке деструктора.

Вы можете даже пойти дальше и создать общий класс WithContext, который принимает дополнительные параметры настройки и функции разрыва.

edit : пришлось переместить вызов action() в отдельную функцию Execute для восстановления безопасности исключений - если он вызывается в конструкторе и выдает, деструктор вызываться не будет.

edit2: общая техника -

Так что я еще немного обдумал эту идею и придумал что-то лучше:
Я определю класс With, который создает переменную контекста и помещает ее в std::auto_ptr в своем инициализаторе, а затем вызывает action:

template <typename T>
class With
{
public:
    template <typename Func>
    With(Func action) : context(new T()) 
    { action(); }

    template <typename Func, typename Arg>
    With(Arg arg, Func action) : context(new T(arg))
    { action(); }

private:
    const std::auto_ptr<T> context;
};

Теперь вы можете комбинировать его с типом контекста, который вы определили изначально:

struct PushedMatrix 
{
    PushedMatrix() { glPushMatrix(); }
    ~PushedMatrix() { glPopMatrix(); }
};

И используйте это так:

With<PushedMatrix>([]
{
    glBegin(GL_LINES);
    //etc. etc.
});

или

With<EnabledFlag>(GL_BLEND, []
{
    //...
});

Преимущества:

  1. Исключительная безопасность теперь обрабатывается auto_ptr, поэтому, если выкинет action, контекст все равно будет уничтожен должным образом.
  2. Больше нет необходимости в методе Execute, поэтому он снова выглядит чистым! :)
  3. Ваши "контекстные" классы очень просты; вся логика обрабатывается классом With, поэтому вам просто нужно определить простой ctor / dtor для каждого нового типа контекста.

Один момент: как я уже писал выше, вам нужно объявить ручные перегрузки для ctor для столько параметров, сколько вам нужно; хотя даже один должен охватывать большинство случаев использования OpenGL, это не очень хорошо. Это должно быть аккуратно исправлено с помощью переменных шаблонов - просто замените typename Arg в ctor на typename ...Args - но это будет зависеть от того, поддерживается ли это компилятором (MSVC2010 пока их не имеет).

22 голосов
/ 22 июля 2010

Использование таких объектов называется RAII и очень типично для управления ресурсами.Да, иногда временные объекты уничтожаются слишком рано, потому что вы забыли указать переменное имя.Но у вас есть одно большое преимущество - код становится более безопасным и чистым от исключений - вам не нужно вручную вызывать все средства для очистки всех возможных путей кода.

Один совет: используйте разумные имена переменных, а не p.Назовите это matrixSwitcher или как-то так, чтобы читатели не думали, что это бесполезная переменная.

6 голосов
/ 22 июля 2010

Как отмечалось другими, это хорошо известный и поощряемый шаблон в C ++.

Чтобы решить проблему забвения имени переменной, нужно определить операции так, чтобы они нуждались в переменной.Либо сделав возможные действия членом класса RAII:

PushedMatrix pushed_matrix;;
pushed_matrix.transform( /*...*/ );

, либо заставив функции принять класс RAII в качестве аргумента:

PushedMatrix pushed_matrix;
transform_matrix( pushed_matrix, /*...*/ );
5 голосов
/ 22 июля 2010

Я хотел бы отметить, что мой ответ на самом деле содержит полезную информацию (более чем смутную ссылку на RAII, которая, по-видимому, стоит 19 голосов). Ему не нужен c ++ 0x для работы, он не является гипотетическим и исправляет проблемы ОП, связанные с необходимостью объявления переменной.


Есть очень хороший способ синтаксически улучшить конструкции RAII (или, точнее, ScopeGuards): оператор if () принимает объявления, ограниченные областью if-block :

#include <stdio.h>

class Lock
{
    public:
    Lock() { printf("locking\n"); }
    ~Lock() { printf("unlocking\n"); }
    operator bool () const { return true;}
};
int main()
{
    // id__ is valid in the if-block only
    if (Lock id_=Lock()) {  
        printf("..action\n");
    }
}

это печатает:

locking
..action
unlocking

Если мы добавим немного синтаксического сахара, мы можем написать

#define WITH(X) if (X with_id_=X())
int main()
{
    WITH(Lock) {    
        printf("..action\n");
        WITH(Lock) {
            printf("more action\n");
        }
    }
}

И теперь мы используем тот факт, что временные ссылки, которые используются для инициализации ссылки на const, остаются живыми до тех пор, пока ссылка на const остается в области действия , чтобы заставить ее работать с параметрами (Мы также исправляем неудобство WITH (X) принимает завершающий остаток):

   #include <stdio.h>
   class ScopeGuard 
   {
    public:
    mutable int dummy;
    operator bool () const { return false;}
    ScopeGuard(){}
    private:
    ScopeGuard(const ScopeGuard &); 
    }; 
    class Lock : public ScopeGuard
    {
        const char *s;
        public: 
        Lock(const char *s_) : s(s_) { printf("locking %s\n",s); }
        ~Lock() { printf("unlocking %s\n",s); }
    };

    #define WITH(X) if (const ScopeGuard& with_id_=X)  {} else 
    int main()
    {
        WITH(Lock("door")) {    
            printf("..action\n");
            WITH(Lock("gate")) {
                printf("more action\n");
            }
        }
    }

TATA!

Приятным побочным эффектом этого метода является то, что все «защищенные» области единообразно идентифицируются с помощью шаблона WITH(...) {...} - хорошее свойство для code-reviews et.al.

4 голосов
/ 22 июля 2010

Предупреждение: C ++ 0x-ориентированный ответ

Используемый вами шаблон RAII, и он широко используется для управления ресурсами. Единственная возможная альтернатива - использовать блоки try-catch, но это обычно делает ваш код слишком запутанным.

Теперь проблемы. Во-первых, если вы не хотите кодировать разные классы для каждой комбинации функций OpenGL, есть еще одно преимущество C ++ 0x, заключающееся в том, что вы можете писать лямбда-функции и сохранять их в переменной. Поэтому на вашем месте я бы создал такой класс:

template<typename Destr>
class MyCustom {
public:
    template<typename T>
    MyCustom(T onBuild, Destr onDestroy) : 
        _onDestroy(std::move(onDestroy))
    {
        onBuild();
    }

    ~MyCustom() { _onDestroy(); }

private:
    Destr    _onDestroy;
};

template<typename T1, typename T2>
MyCustom<T2> buildCustom(T1 build, T2 destruct)   { return MyCustom<T2>(std::move(build), std::move(destruct)); }

Тогда вы можете использовать это так:

auto matrixPushed = buildCustom([]() { glPushMatrix(); }, []() { glPopMatrix(); });

Или еще лучше здесь:

auto matrixPushed = buildCustom(&glPushMatrix, &glPopMatrix);

Это также решило бы проблему «почему эта бесполезная переменная здесь», поскольку ее назначение теперь становится очевидным.

Функция, передаваемая в конструктор, должна быть встроенной, чтобы не было проблем с производительностью. Деструктор должен храниться как указатель на функцию, поскольку лямбда-функции, не содержащие ничего в скобках [], должны быть реализованы как обычные функции (согласно стандартам).

Ваша проблема «мгновенного уничтожения переменной» также будет частично решена с помощью «buildCustom», так как вам будет легче увидеть, где вы забыли переменную.

3 голосов
/ 22 июля 2010

Чтобы помочь вам понять, как долго программисты на С ++ занимались этим, я узнал об этой технике в конце 90-х, работая с COM.

Я думаю, что это личный выбор относительно того, какой именно механизм вы используете для использования.фундаментальные свойства стековых фреймов и деструкторов c ++, упрощающие управление временем жизни вашего объекта.Я бы не стал уходить слишком далеко, чтобы избежать необходимости присваивать переменную.

(это следующая вещь, в которой я не уверен на 100%, но я надеюсь, что кто-то перезвонитв - я знаю, что делал это в прошлом, но я не мог найти это в Google только сейчас, и я пытался вспомнить ... видите, сборщики мусора притупили мой разум!)

Я полагаю, что вы можете форсировать прицел простой старой парой кудряшек (POPOC).

{ // new stack frame
  auto_ptr<C> instanceA(new C);
  {
     auto_ptr<C> instanceB(new C);
  }
  // instanceB is gone
} 
// instanceA is gone
1 голос
/ 23 июля 2010

ScopeGuard приходит на ум. Обратите внимание, что в шаблонах C ++ 0x bind и variadic его можно переписать так, чтобы он был намного короче.

1 голос
/ 22 июля 2010

Это типичный пример RAII.Недостатком этого метода является появление множества дополнительных классов.Чтобы решить эту проблему, вы можете создать общий класс «guard», если это возможно.Есть и другая альтернатива: повысить библиотеку «Выход из области видимости» (http://www.boost.org/doc/libs/1_43_0/libs/scope_exit/doc/html/index.html).. Вы можете попробовать ее, если конечно можете зависеть от повышения.

0 голосов
/ 22 июля 2010

Я думаю, что это отличный идиоматический C ++.Недостатком является то, что вы в основном пишете (пользовательский) Wrapper вокруг библиотеки C OpenGL.Было бы замечательно, если бы существовала такая библиотека, может быть, что-то вроде (полу) официальной библиотеки OpenGL ++.Тем не менее, я написал такой код (из памяти), и был очень доволен им:

{
  Lighting light = Light(Color(128,128,128));
    light.pos(0.0, 1.0, 1.0);
  Texture tex1 = Texture(GL_TEXTURE1);
    tex1.set(Image("CoolTex.png"));

  drawObject();
}

Затраты на написание оболочек не очень обременительны, и полученный код так же хорошрукописный код.И ИМХО гораздо легче читать, чем соответствующий код OpenGL, даже если вы не знаете обертки наизусть.

0 голосов
/ 22 июля 2010

Я никогда не видел этого раньше, но должен признать, это несколько круто.Но я бы не стал его использовать, так как он не совсем интуитивен.

РЕДАКТИРОВАТЬ: Как указал острый зуб, это называется RAII.Пример, который я нашел в википедии, также включает операции с ресурсом в вызовах методов.В вашем примере это будет выглядеть следующим образом:

WithPushedMatrix p;
p.setFLag(GL_BLEND);
p.doSomething();

Тогда понятно, что это за переменная, и другие разработчики получат интуицию, если они прочитают ваш код.Конечно, тогда код OpenGL скрыт, но я думаю, что к нему быстро привыкают.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...