最简单的解决方案是在未命名的结构体中放置一个命名结构体实例,并将所有功能放入命名实例中。这可能是与C++98兼容的唯一方法。
#include <iostream>
#include <cmath>
int main() {
struct {
struct S {
double a;
int b;
S() : a(sqrt(4)), b(42) { std::cout << "constructed" << std::endl; }
~S() { std::cout << "destructed" << std::endl; }
} s;
} instance1, instance2;
std::cout << "body" << std::endl;
}
接下来的所有内容都需要C++11值初始化支持。
为了避免嵌套,构建的解决方案很简单。您应该使用C++11值初始化来初始化所有成员。您可以使用lambda调用的结果对它们进行初始化,因此在初始化期间可以执行任意复杂的代码。
#include <iostream>
#include <cmath>
int main() {
struct {
double a { sqrt(4) };
int b { []{
std::cout << "constructed" << std::endl;
return 42; }()
};
} instance1, instance2;
}
当然,你可以把所有“构造函数”代码放到一个单独的成员中:
int b { [this]{ constructor(); return 42; }() };
void constructor() {
std::cout << "constructed" << std::endl;
}
这段代码仍然不够清晰,并且将b
的初始化与其他内容混淆在一起。你可以将constructor
调用移动到一个帮助类中,代价是空类仍然占据未命名结构体中的一些空间(如果它是最后一个成员,则通常为1个字节)。
#include <iostream>
#include <cmath>
struct Construct {
template <typename T> Construct(T* instance) {
instance->constructor();
}
};
int main() {
struct {
double a { sqrt(4) };
int b { 42 };
Construct c { this };
void constructor() {
std::cout << "constructed" << std::endl;
}
} instance1, instance2;
}
由于 c
实例需要一些空间,我们不妨明确指出,并摆脱辅助程序。下面的代码闻起来像是 C++11 的成语,但由于返回语句有点冗长。
struct {
double a { sqrt(4) };
int b { 42 };
char constructor { [this]{
std::cout << "constructed" << std::endl;
return char(0);
}() };
}
为了获得析构函数,您需要助手来存储包装类实例的指针和调用该实例上的析构函数的函数指针。由于我们只能在助手的构造函数中访问未命名结构体类型,因此我们必须在那里生成调用析构函数的代码。请注意保留HTML标签。
#include <iostream>
#include <cmath>
struct ConstructDestruct {
void * m_instance;
void (*m_destructor)(void*);
template <typename T> ConstructDestruct(T* instance) :
m_instance(instance),
m_destructor(+[](void* obj){ static_cast<T*>(obj)->destructor(); })
{
instance->constructor();
}
~ConstructDestruct() {
m_destructor(m_instance);
}
};
int main() {
struct {
double a { sqrt(4) };
int b { 42 };
ConstructDestruct cd { this };
void constructor() {
std::cout << "constructed" << std::endl;
}
void destructor() {
std::cout << "destructed" << std::endl;
}
} instance1, instance2;
std::cout << "body" << std::endl;
}
现在你一定在抱怨ConstructDestruct
实例中存储的数据冗余。实例存储的位置是从未命名结构体头部的固定偏移量处开始的。你可以获取这样的偏移量并将其包装在一个类型中(见此处)。因此我们可以摆脱ConstructorDestructor
中的实例指针:
#include <iostream>
#include <cmath>
#include <cstddef>
template <std::ptrdiff_t> struct MInt {};
struct ConstructDestruct {
void (*m_destructor)(ConstructDestruct*);
template <typename T, std::ptrdiff_t offset>
ConstructDestruct(T* instance, MInt<offset>) :
m_destructor(+[](ConstructDestruct* self){
reinterpret_cast<T*>(reinterpret_cast<uintptr_t>(self) - offset)->destructor();
})
{
instance->constructor();
}
~ConstructDestruct() {
m_destructor(this);
}
};
#define offset_to(member)\
(MInt<offsetof(std::remove_reference<decltype(*this)>::type, member)>())
int main() {
struct {
double a { sqrt(4) };
int b { 42 };
ConstructDestruct cd { this, offset_to(cd) };
void constructor() {
std::cout << "constructed " << std::hex << (void*)this << std::endl;
}
void destructor() {
std::cout << "destructed " << std::hex << (void*)this << std::endl;
}
} instance1, instance2;
std::cout << "body" << std::endl;
}
很遗憾,似乎无法从ConstructDestruct
中消除函数指针。不过这没关系,因为它的大小必须是非零的。无论如何,在未命名结构体之后存储的内容很可能会被对齐到函数指针大小的倍数,因此sizeof(ConstructDestruct)
比1大时可能没有开销。