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.