C++ - 在初始化类成员之前运行函数

C++ - Run a function before initializing a class member

本文关键字:运行 函数 成员 初始化 C++      更新时间:2023-10-16

我有 2 个资源管理类DeviceContextOpenGLContext两者都是class DisplayOpenGL的成员。资源生存期与DisplayOpenGL相关联。初始化如下所示(伪代码):

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

问题是对 SetPixelFormat() 的调用,因为我无法在DisplayOpenGLc'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() 没有 retval。(如果它有 reval,你应该这样做吗?
  • 使用unique_ptr<OpenGLContext> m_opengl;而不是OpenGLContext m_opengl;
    然后初始化为m_opengl(),在 c'tor 主体中调用 SetPixelFormat() 并使用m_opengl.reset(new OpenGLContext);
  • DeviceContextc'tor拨打SetPixelFormat()

这些解决方案中哪一个更可取,为什么?我错过了什么?

我正在Windows上使用Visual Studio 2010 Express,如果它很重要的话。

编辑:我最感兴趣的是决定其中一种方法所涉及的权衡。

  • m_dummy()不起作用,即使它会
  • unique_ptr<X>对我来说很有趣 - 我什么时候会使用它而不是"普通"X m_x成员?除了初始化问题之外,这两种方法在功能上似乎或多或少是等效的。
  • DeviceContextc'tor 打电话给SetPixelFormat()当然有效,但对我来说感觉不干净。DeviceContext应该管理资源并启用其使用,而不是对用户施加一些随机像素格式策略。
  • Stijn的InitDev()看起来是最干净的解决方案。

在这种情况下,我几乎总是想要一个基于智能指针的解决方案吗?

逗号运算符来救援!表达式(a, b)将首先计算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;
};

在这种情况下,我几乎总是想要一个基于智能指针的解决方案吗?

不。避免这种不必要的复杂化。

尚未提及的两种直接方法:

方法A:

干净的方式。

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;
};

方法B:

快速而肮脏的方式。在这种情况下,您可以使用静态函数:

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;
};

首先,你做错了。 :-) 在构造函数中做复杂的事情是非常糟糕的做法。 曾。 使这些操作在必须传递到构造函数的帮助程序对象上函数。 更好的方法是在类之外构造复杂对象并将它们完全创建,这样如果您需要将它们传递给其他类,您也可以同时将它们传递给 ITS 构造函数。 此外,这样你就有机会检测错误,添加合理的日志记录等。

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,你可以用 lambda 替换 InitDev()。

如果它属于DeviceContext(从你的代码来看似乎是这样),请从DeviceContextc'tor调用它。

将逗号运算符与 IIFE(立即调用的函数表达式)结合使用,它允许您定义变量和其他仅使用逗号运算符不可用的复杂内容:

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

逗号运算符在您的情况下会做得很好,但我认为这个问题是您的类规划不善的结果。我要做的是让构造函数只初始化对象的状态,而不是依赖项(例如 OpenGL 渲染上下文)。我假设OpenGLContext的构造函数初始化OpenGL渲染上下文,这是我不会做的。相反,我会为 OpenGLContext 类创建方法CreateRenderingContext来执行初始化并调用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();
}

逗号运算符是我想到的第一件事。构造函数链接还可以让你稍微整理一下。

但是,我认为我已经想出了一种更整洁的方法,使您的意图清晰,并且围绕成员的实际初始化减少混乱 - 这在监视资源管理时很重要。


// 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

};

可在此处作为要点提供