C++: разрешение зависимостей списка инициализаторов конструктора с помощью RAII

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

Вот что я написал на C (только соответствующие части).

int gfx_init(struct gfx *g)
{
    int vis_attriblist[] = { GLX_RGBA, GLX_DOUBLEBUFFER, None };
    XSetWindowAttributes wa;
    XVisualInfo *vis_info;
    int r = 0;

    g->xdpy = XOpenDisplay(NULL);
    if (g->xdpy == NULL) {
            r = -1;
            LOG_ERROR("Could not open X Display");
            goto xopendisplay_failed;
    }

    vis_info = glXChooseVisual(g->xdpy, DefaultScreen(g->xdpy),
                               vis_attriblist);
    if (vis_info == NULL) {
            r = -1;
            LOG_ERROR("Couldn't get an RGBA, double-buffered visual"
                      " (GLX available?)\n");
            goto glxchoosevisual_failed;
    }

    g->xcolormap = XCreateColormap(g->xdpy, DefaultRootWindow(g->xdpy),
                                   vis_info->visual, AllocNone);
    if (gfx_has_xerror(g) /* Checks if there are errors
                             by flushing Xlib's protocol buffer
                             with a custom error handler set.
                             Not included here */) {
            r = -1;
            LOG_ERROR("Failed to create colormap");
            goto xcreatecolormap_failed;
    }

    wa.colormap = g->xcolormap;
    wa.event_mask = StructureNotifyMask | VisibilityChangeMask;

    g->xwindow = XCreateWindow(g->xdpy, DefaultRootWindow(g->xdpy),
                               0,0,1280,1024, 0, vis_info->depth,
                               InputOutput, vis_info->visual, CWColormap |
                               CWEventMask, &wa);
    if (gfx_has_xerror(g)) {
            r = -1;
            LOG_ERROR("Failed to create X11 Window");
            goto xcreatewindow_failed;
    }

    g->glxctx = glXCreateContext(g->xdpy, vis_info, NULL, True);
    if (g->glxctx == NULL) {
            r = -1;
            LOG_ERROR("Failed to create GLX context");
            goto glxcreatecontext_failed;
    }

    if (glXMakeCurrent(g->xdpy, g->xwindow, g->glxctx) == False) {
            r = -1;
            LOG_ERROR("Failed to make context current");
            goto glxmakecurrent_failed;
    }

    g->xa_wmdeletewindow = XInternAtom(g->xdpy, "WM_DELETE_WINDOW", False);
    if (gfx_has_xerror(g)) {
            r = -1;
            LOG_ERROR("XInternAtom failed");
            goto xinternatom_failed;
    }

    XSetWMProtocols(g->xdpy, g->xwindow, &g->xa_wmdeletewindow, 1);
    if (gfx_has_xerror(g)) {
            r = -1;
            LOG_ERROR("XSetWMProtocols failed");
            goto xsetwmprotocols_failed;
    }

    glClearColor(1,1,1,1);
    glColor4f(0,0,0,1);
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    if (glGetError() != GL_NO_ERROR) {
            r = -1;
            LOG_ERROR("There have been GL errors");
            goto gotglerror;
    }

    XMapWindow(g->xdpy, g->xwindow);
    XFlush(g->xdpy);

    if (r < 0) {
gotglerror:
xsetwmprotocols_failed:
xinternatom_failed:
            glXMakeCurrent(g->xdpy, None, NULL);
glxmakecurrent_failed:
            glXDestroyContext(g->xdpy, g->glxctx);
glxcreatecontext_failed:
            XDestroyWindow(g->xdpy, g->xwindow);
xcreatewindow_failed:
            XFreeColormap(g->xdpy, g->xcolormap);
    }

xcreatecolormap_failed:
    /* This is a local resource which must be destroyed
       in case of success as well */
    XFree(vis_info);

    if (r < 0) {
glxchoosevisual_failed:
            XCloseDisplay(g->xdpy);
    }

xopendisplay_failed:
    return r;
}

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

Я хотел бы знать, как выполнить эту инициализацию в хорошем стиле RAII C++. В частности, существует зависимость между распределением элементов, и воображаемый конструктор RAII class Gfx должен инициализироваться в правильном порядке или выдать исключение и гарантировать, что исходная созданная часть снова будет удалена.

Таким образом, естественным прогрессом является написание коротких оболочек для выделенных типов. Например.

struct MyDisplay {
    Display *dpy;
    MyDisplay() : dpy(XOpenDisplay(NULL)) { if (!dpy) throw "XOpenDisplay()"; }
    ~MyDisplay() { XCloseDisplay(dpy); }
};
struct MyXVisualInfo {
    XVisualInfo *info;
    static int vis_attriblist[] = { GLX_RGBA, GLX_DOUBLEBUFFER, None };
    MyXVisualInfo(Display *dpy)
            : info(glXChooseVisual(dpy, DefaultScreen(dpy), vis_attriblist) {
        if (!info)
            throw "glXChooseVisual()";
   }
   ~MyXVisualInfo() {
       XFree(info);
   }
};

И класс Gfx:

class Gfx {
    MyDisplay mydpy_;
    MyXVisualInfo myinfo_;
    /* ... */
public:
    Gfx::Gfx() : mydpy_(), myinfo_(mydpy_.dpy) /* , ... */ {}
};

Но на данный момент у нас есть проблема: myinfo_(mydpy.dpy) на самом деле передает неопределенное значение конструктору MyXVisualInfo. Является ли это препятствием для членов класса RAII? (это неправда, см. ответ @MarkB)

Кроме того, если конструктору необходимо выделить временные ресурсы, как это имеет место для myinfo_, которые я не хотел бы хранить внутри класса, я думаю, что нет способа избежать входа в тело конструктора, и присвоения членам оттуда, используя ресурсы, локальные для тела конструктора. Это означает, что участники будут подвергаться дополнительной конструкции и деконструкции, что недопустимо, поскольку есть побочные эффекты.

Единственное, о чем я могу думать, это использовать unique_ptr:

class Gfx {
    unique_ptr<MyDisplay> mydpy_;
    unique_ptr<MyColormap> mycolormap_;
    /* ... */
public:
    Gfx() {
        mydpy_.reset(new MyDisplay());

        MyXVisualInfo myinfo(dpy);

        mycolormap_.reset(new MyXColormap(mydpy_->dpy,
                                          DefaultRootWindow(mydpy_->dpy),
                                          myinfo->info, AllocNone));
    }
};

Теперь это явно перепроектировано и стало кладжом для обслуживания. Также нехорошо вводить unique_ptr, который имеет ненужные накладные расходы.

Разве нет чистого подхода, который делал бы то, что делает версия C, в чистой манере RAII?


person Jo So    schedule 26.11.2013    source источник


Ответы (2)


std::shared_ptr и std::unique_ptr можно использовать с пользовательским средством удаления. Пример:

std::shared_ptr<int> mem (static_cast<int*>(malloc(sizeof(int))), free);

или с лямбдой:

std::shared_ptr<int> mem (new int(), [](int* foobar) {
    std::cout << "I am a deleter" << std::endl;
    delete foobar;
});
person Sebastian Mach    schedule 26.11.2013
comment
Думаю, это именно то, что мне нужно. Для случаев, когда у меня нет указателей, но есть структуры POD, я уже думал о создании подклассов структур POD, чтобы добавить удаление RAII. Ваш ответ показывает, как распространить эту идею на указатели: конструкция семантически является подклассом содержащихся указателей. - person Jo So; 26.11.2013
comment
Я вижу, что умные указатели больше похожи на передачу ответственности за управление памятью тому, кто в этом хорош. Необработанные указатели на самом деле трудно, если не иногда бесчеловечно, исправить, когда поблизости находятся исключения C++. - person Sebastian Mach; 26.11.2013
comment
Дополнение для обучения (как и я): лично я не являюсь поклонником shared_ptr, поскольку он делает уничтожение менее явным и менее детерминированным, что является недостатком, особенно для побочных эффектов. Вместо этого я выберу unique_ptr. Использование также очень удобно, используя &*: unique_ptr<Display, mydeleter> dpy(XOpenDisplay(NULL)); glxChooseVisual(&*dpy, ... - person Jo So; 26.11.2013

Что привело вас к выводу, что myinfo(mydpy.dpy) будет иметь неопределенное поведение? Вопрос SO, который вы связали, отличается от того же сценария, что и в вашем образце кода. Обратите внимание, что в вашем случае вы перечисляете члены в определении класса в том порядке, в котором вы хотите, чтобы они были инициализированы, чтобы не было неопределенного поведения.

Как правило, если вы чувствуете, что не можете сделать что-то, что вам нужно в инициализированном списке, и вам нужно использовать тело конструктора, вы можете поочередно выделить код в функцию и использовать возвращаемое значение из этого в списке инициализатора, но у меня есть действительно трудно понять, что вы имеете в виду после Also, if the constructor needs to allocate temporary resources, поэтому я не могу дать вам лучший ответ, чем этот.

person Mark B    schedule 26.11.2013
comment
Примером для temporary resources является vis_info из версии C. - person Jo So; 26.11.2013
comment
Если я выделю статические функции и использую возвращаемое значение в списке инициализаторов, это все равно будет деконструировать/конструировать, если я не использую семантику перемещения и не добавляю условный разрыв, который очень тяжело реализуем. Я бы предпочел другой подход. - person Jo So; 26.11.2013
comment
+1: Спасибо, что поправили меня в части заказа - я полностью ошибся в этом другом сообщении SO. - person Jo So; 26.11.2013