Подтвердить что ты не робот

(Ab) с использованием конструкторов и деструкторов для побочных эффектов плохой практики? Альтернативы?

В OpenGL часто записывается такой код:

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

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

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

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

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

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

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

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

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

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

WithEnabledFlags(GL_BLEND); // whoops!

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

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

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

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

4b9b3361

Ответ 1

Мне нравится идея использования RAII для управления состоянием OpenGL, но я бы сделал это еще на один шаг: пусть ваш конструктор класса WithFoo возьмет указатель на функцию в качестве параметра, который содержит код, который вы хотите выполнить в этот контекст. Тогда не создавайте именованные переменные и просто работайте со временными, передавая действие, которое вы хотите выполнить в этом контексте, как лямбда. (требуется С++ 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, []
{
    //...
});

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

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

PushedMatrix pushed_matrix;
transform_matrix( pushed_matrix, /*...*/ );

Ответ 4

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


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

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

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

   #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(...) {...} - приятное свойство для кодовых обзоров et.al.

Ответ 5

Внимание: С++ 0x-ориентированный ответ

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

Теперь проблемы. Во-первых, если вы не хотите кодировать другой класс для каждой комбинации функций OpenGL, есть еще одно преимущество С++ 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", поскольку вы можете более легко увидеть, где вы забыли переменную.

Ответ 6

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

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

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

Я считаю, что вы можете заставить область с простой старой фигурой (POPOC).

{ // new stack frame
  auto_ptr<C> instanceA(new C);
  {
     auto_ptr<C> instanceB(new C);
  }
  // instanceB is gone
} 
// instanceA is gone

Ответ 7

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

Ответ 8

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

Ответ 9

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

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

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

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

Ответ 10

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

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

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