如何向一个未命名的类添加构造函数/析构函数?

48

在无名类中是否有一种声明构造函数或析构函数的方法?考虑以下情况

void f()
{
    struct {
        // some implementation
    } inst1, inst2;

    // f implementation - usage of instances
}

跟进问题:这些实例当然像任何基于堆栈的对象一样被构造(和销毁)。 调用的是什么? 这是编译器自动分配的缩略名称吗?

跟进问题:这些实例当然像任何基于堆栈的对象一样被构造(和销毁)。调用的是什么?这是编译器自动分配的缩略名称吗?


1
@πάνταῥεῖ 显然我需要睡一会儿了。 - juanchopanza
3
这个问题很明显是探索性的。我知道你不能直接这样做,至少不是通常的方式。标准规定:构造函数没有名称。使用一种特殊的声明符语法来声明或定义构造函数,其中包括一个可选的函数说明符 (7.1.2),后跟构造函数的类名,再后面是参数列表。如果你没有名称,就无法这样做。我感兴趣的是,是否存在解决方法以及实际调用的机制。 - Nikos Athanasiou
2
为什么不给它们命名呢?这样起码可以更容易地进行调试。 - Ed Heal
2
我并不是在暗示我的代码中有一个我拒绝命名的类型,也不是匿名类是我的一种实践。我只是在探索隐式调用构造函数/析构函数的机制,并寻找一种解决方法,这对于我在学术上非常感兴趣。如果问题看起来像是我的问题,我很抱歉。 - Nikos Athanasiou
1
@NikosAthanasiou "...也不是匿名类是我的一种做法..." 从技术上讲,这是一个未命名的类(正如您在问题中所述),而不是“匿名类”。未命名的类是允许的,匿名类不允许(尽管在C11中允许,在C++11中不允许,但这可能会改变)。 - monkey0506
显示剩余4条评论
3个回答

49
最简单的解决方案是在未命名的结构体中放置一个命名结构体实例,并将所有功能放入命名实例中。这可能是与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大时可能没有开销。


非常有趣的方法!特别是使用lambda构造的char。注意,在VC++中,它会抱怨将0强制转换为char(因为返回值没有明确指定为char)。也许使用bool更清晰? - Elliot Woods
在我的看法中,值初始化技巧似乎不能与未命名的class/struct基础类型一起使用。此外,它也不能(合理地)为成员提供构造参数。但还是很不错的知识! - Adam Badura

18

无法为匿名类声明构造函数或析构函数,因为构造函数和析构函数的名称需要与类名称匹配。在您的示例中,该匿名类是本地类。它没有链接,因此不会创建任何名称。


我不认为这是一个有用的答案。它没有回答任何问题,只是简单地陈述了“在当前规范下,你不能这样做”,而且在实现上是错误的。从编译器的角度来看,我不明白为什么它不能为这样的构造函数自动生成一个混淆的名称。它已经为lambda函数等做到了这一点。因此,这个解释非常无意义。 - c00000fd
@c00000fd 未命名类是由用户定义的,而Lambda则是由编译器定义的。因此它们之间存在很大的区别。你的评论没有意义。问题是关于用户如何为未命名类声明构造函数或析构函数,但他没有这样的可能性。 - Vlad from Moscow

2
如果您考虑C++名称,那么任何具有对象的类都必须具有析构函数,无论您是否显式创建它。因此,编译器知道如何分配名称。然而,命名约定是否与您有关,可能并不是您所关心的事情。
实际上,您可以创建一个没有名称的结构或命名空间。您仍然需要在某个地方使用名称,因为在链接所有内容时,链接器需要一些名称才能使其正常工作,尽管在许多情况下,这将是本地名称,在汇编时立即解析。
了解编译器分配的名称的一种方法是查看调试字符串,并查看与您感兴趣的不同地址对应的内容。当您使用-g编译时,您应该获得所有必要的调试信息,以便您的调试器将当前位置放置在正确的位置并具有正确的“名称”...(我看到了没有名称的命名空间,它说“namespace”,我非常确定结构在更高级别上使用相同的技巧。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接