C++ - запустить функцию перед инициализацией члена класса

У меня есть 2 класса управления ресурсами DeviceContext а также OpenGLContext оба являются членами class DisplayOpenGL, Время жизни ресурса связано с DisplayOpenGL, Инициализация выглядит следующим образом (псевдокод):

DeviceContext m_device = DeviceContext(hwnd);
m_device.SetPixelFormat();
OpenGLContext m_opengl = OpenGLContext(m_device);

Проблема заключается в вызове SetPixelFormat(), так как я не могу сделать это в списке инициализатора DisplayOpenGL c'tor:

class DisplayOpenGL {
public:
    DisplayOpenGL(HWND hwnd)
    : m_device(hwnd),
      // <- Must call m_device.SetPixelFormat here ->
      m_opengl(m_device) { };
private:
    DeviceContext m_device;
    OpenGLContext m_opengl;
};

Решения, которые я вижу:

  • Вставка m_dummy(m_device.SetPixelFormat()) - Не будет работать, так как SetPixelFormat () не имеет обратного хода. (Вы должны сделать это, если у него был ретваль?)
  • использование unique_ptr<OpenGLContext> m_opengl; вместо OpenGLContext m_opengl;,
    Затем инициализируйте как m_opengl() вызовите SetPixelFormat () в теле c'tor и используйте m_opengl.reset(new OpenGLContext);
  • Вызов SetPixelFormat() от DeviceContext c'tor

Какое из этих решений предпочтительнее и почему? Что-то, что мне не хватает?

Я использую Visual Studio 2010 Express в Windows, если это имеет значение.

Редактировать: меня больше всего интересуют компромиссы, связанные с выбором одного из этих методов.

  • m_dummy() не работает и кажется не элегантным, даже если бы
  • unique_ptr<X> мне интересно - когда бы я использовал его вместо "нормального" X m_x член? Эти два метода кажутся функционально более или менее эквивалентными, за исключением проблем инициализации.
  • призвание SetPixelFormat() от DeviceContext не может, конечно, работает, но кажется мне нечистым. DeviceContext должен управлять ресурсом и разрешать его использование, а не навязывать пользователям политику произвольного формата пикселей.
  • Стин - х InitDev() выглядит как самое чистое решение.

В любом случае, всегда ли мне нужно решение на основе умного указателя в таких случаях?

9 ответов

Решение

Comma operator to the rescue! Выражение (a, b) will evaluate a будет первый b,

class DisplayOpenGL {
public:
    DisplayOpenGL(HWND hwnd)
    : m_device(hwnd),
      m_opengl((m_device.SetPixelFormat(), m_device)) { };
private:
    DeviceContext m_device;
    OpenGLContext m_opengl;
};

В любом случае, всегда ли мне нужно решение на основе умного указателя в таких случаях?

Нет. Избегайте этого ненужного осложнения.

Два непосредственных подхода, которые не были упомянуты:

Подход А:

Чистый путь.

Создайте маленький контейнерный объект для m_deviceхранилище звонков SetPixelFormat() в конструкторе. Затем заменить DisplayOpenGL ::m_device с экземпляром этого типа. Порядок инициализации получен, и цель вполне понятна. Иллюстрация:

class DisplayOpenGL {
public:
    DisplayOpenGL(HWND hwnd)
        : m_device(hwnd),
            m_opengl(m_device) { }
private:
    class t_DeviceContext {
    public:
        t_DeviceContext(HWND hwnd) : m_device(hwnd) {
            this->m_device.SetPixelFormat();
        }
        // ...
    private:
        DeviceContext m_device;
    };
private:
    t_DeviceContext m_device;
    OpenGLContext m_opengl;
};

Подход Б:

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

class DisplayOpenGL {
public:
    DisplayOpenGL(HWND hwnd)
    : m_device(hwnd),
      m_opengl(InitializeDevice(m_device)) { }
private:
    // document why it must happen this way here
    static DeviceContext& InitializeDevice(DeviceContext& pDevice) {
      pDevice.SetPixelFormat();
      return pDevice;
    }
private:
    DeviceContext m_device;
    OpenGLContext m_opengl;
};

Прежде всего, вы делаете это неправильно.:-) Это очень плохая практика делать сложные вещи в конструкторах. Когда-либо. Сделайте эти операции функциями вспомогательного объекта, который вместо этого должен быть передан в конструктор. Лучше создать свои сложные объекты за пределами вашего класса и передать их полностью созданными. Таким образом, если вам нужно передать их другим классам, вы можете сделать это и в конструкторы ИХ одновременно. Кроме того, у вас есть возможность обнаруживать ошибки, добавлять логичные записи и т. Д.

class OpenGLInitialization
{
public:
    OpenGLInitialization(HWND hwnd)
        : mDevice(hwnd) {}
    void                 SetPixelFormat  (void)       { mDevice.SetPixelFormat(); }
    DeviceContext const &GetDeviceContext(void) const { return mDevice; }
private:
    DeviceContext mDevice;
};        

class DisplayOpenGL 
{
public:
    DisplayOpenGL(OpenGLInitialization const &ogli)
    : mOGLI(ogli),
      mOpenGL(ogli.GetDeviceContext())
      {}
private:
    OpenGLInitialization mOGLI;
    OpenGLContext mOpenGL;
};

Если OpenGLContext имеет конструктор аргумента 0 и конструктор копирования, который вы можете изменить на конструктор

DisplayOpenGL(HWND hwnd)
: m_device(hwnd)
{
    m_device.SetPixelFormat();
    m_opengl = OpenGLContext(m_device);
};

unique_ptr обычно используется, когда вы хотите сделать одного из участников необязательным или "обнуляемым", что вы можете или не можете делать здесь.

Использование uniqe_ptr для обоих вариантов представляется здесь уместным: вы можете пересылать объявления DeviceContext и OpenGLContext вместо включения их заголовков, что хорошо). Тогда это работает:

class DisplayOpenGL
{
public:
  DisplayOpenGL( HWND h );
private:
  unique_ptr<DeviceContext> m_device;
  unique_ptr<OpenGLContext> m_opengl;
};

namespace
{
  DeviceContext* InitDev( HWND h )
  {
    DeviceContext* p = new DeviceContext( h );
    p->SetPixelFormat();
    return p;
  }
}

DisplayOpenGL::DisplayOpenGL( HWND h ):
  m_device( InitDev( h ) ),
  m_opengl( new OpenGLContext( *m_device ) )
{
}

Если вы можете использовать C++11, вы можете заменить InitDev() на лямбду.

Оператор запятой в вашем случае будет работать хорошо, но я думаю, что эта проблема является следствием плохого планирования ваших классов. Что бы я сделал, чтобы позволить конструкторам только инициализировать состояние объектов, а не их зависимости (например, контекст рендеринга OpenGL). Я предполагаю, что конструктор OpenGLContext инициализирует контекст рендеринга OpenGL, и я бы этого не делал. Вместо этого я бы создал метод CreateRenderingContext для класса OpenGLContext для инициализации, а также для вызова SetPixelFormat

class OpenGLContext {
public:
    OpenGLContext(DeviceContext* deviceContext) : m_device(deviceContext) {}
    void CreateRenderingContext() {
        m_device->SetPixelFormat();
        // Create the rendering context here ...
    }
private: 
    DeviceContext* m_device;
};

...

DisplayOpenGL(HWND hwnd) : m_device(hwnd), m_opengl(&m_device) {
    m_opengl.CreateRenderingContext();
}

Объедините оператор запятой с IIFE (выражением немедленного вызова функции), которое позволяет вам определять переменные и другие сложные вещи, недоступные только с помощью оператора запятой:

struct DisplayOpenGL {
    DisplayOpenGL(HWND hwnd)
        : m_device(hwnd)
        , opengl(([&] {
            m_device.SetPixelFormat();
        }(), m_device))
    DeviceContext m_device;
    OpenGLContext m_opengl;
};

Оператор запятая был первым, о чем я подумал. Цепочка конструкторов также позволяет немного навести порядок.

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

      
// Inherit from me privately.
struct ConstructorPreInitialisation{
    // Pass in an arbitrary lambda function here, it will simply be discarded
    // This remoes the need for a comma operator and importantly avoids cluttering your
    // real initialisation of member subobjects.
    inline ConstructorPreInitialisation( [[maybe_unused]] const auto λ ){ λ(); }
};
// WARN: This may increase the size of your class using it
// The compiler can probably elide this but from memory objects with zero length are not permitted
// Have not checked the fine details against the standard
// Therefore this should not be used if this is an unacceptable condition



// Usage
// Example class originally from: https://en.cppreference.com/w/cpp/language/constructor#Example

#include <fstream>
#include <string>
#include <mutex>
 
struct Base
{
    int n;
};   
 
struct Class : public Base, private ConstructorPreInitialisation
{
    unsigned char x;
    unsigned char y;
    std::mutex m;
    std::lock_guard<std::mutex> lg;
    std::fstream f;
    std::string s;
 
    Class(int x) 
      : Base{123}, // initialize base class
      
        ConstructorPreInitialisation([&](){
            // Call some global allocation here, for example.
        }),
        
        x(x),     // x (member) is initialized with x (parameter)
        y{0},     // y initialized to 0
        f{"test.cc", std::ios::app}, // this takes place after m and lg are initialized
        s(__func__), // __func__ is available because init-list is a part of constructor
        lg(m),    // lg uses m, which is already initialized
        m{}       // m is initialized before lg even though it appears last here
    {}            // empty compound statement
 
};
 

Краткое описание доступно здесь

Если это принадлежит DeviceContext (и это похоже на ваш код), позвоните из DeviceContext c'tor.

Другие вопросы по тегам