有什么方法可以为将来的添加准备一个结构吗?

发布于 2024-09-27 13:41:20 字数 612 浏览 5 评论 0原文

我有以下结构将用于保存插件信息。我非常确定这会随着时间的推移而改变(很可能会增加)。假设该文件将被修复,还有比我所做的更好的事情吗?

struct PluginInfo
{
    public:
        std::string s_Author;
        std::string s_Process;
        std::string s_ReleaseDate;
        //And so on...

        struct PluginVersion
        {
            public:
                std::string s_MajorVersion;
                std::string s_MinorVersion;
                //And so on...
        };
        PluginVersion o_Version;

        //For things we aren't prepared for yet.
        void* p_Future;
};

此外,在为此系统构建共享对象时我应该采取任何预防措施吗?我的预感是我会遇到很多库不兼容的情况。请帮忙。谢谢

I have the following struct which will be used to hold plugin information. I am very sure this will change (added to most probably) over time. Is there anything better to do here than what I have done assuming that this file is going to be fixed?

struct PluginInfo
{
    public:
        std::string s_Author;
        std::string s_Process;
        std::string s_ReleaseDate;
        //And so on...

        struct PluginVersion
        {
            public:
                std::string s_MajorVersion;
                std::string s_MinorVersion;
                //And so on...
        };
        PluginVersion o_Version;

        //For things we aren't prepared for yet.
        void* p_Future;
};

Further, is there any precautions I should take when building shared objects for this system. My hunch is I'll run into lots of library incompatibilities. Please help. Thanks

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

怪我入戏太深 2024-10-04 13:41:20

这是怎么回事,还是我想得太简单了?

struct PluginInfo2: public PluginInfo
{
    public:
        std::string s_License;
};

在您的应用程序中,您可能只传递指向 PluginInfo 的指针,因此版本 2 与版本 1 兼容。当您需要访问版本 2 成员时,您可以使用 测试该版本Dynamic_cast 或使用显式 pluginAPIVersion 成员。

What about this, or am I thinking too simple?

struct PluginInfo2: public PluginInfo
{
    public:
        std::string s_License;
};

In your application you are probably passing around only pointers to PluginInfos, so version 2 is compatible to version 1. When you need access to the version 2 members, you can test the version with either dynamic_cast<PluginInfo2 *> or with an explicit pluginAPIVersion member.

巴黎夜雨 2024-10-04 13:41:20

你的插件要么是使用相同版本的 C++ 编译器和 std 库源编译的(或者它的 std::string 实现可能不兼容,并且所有字符串字段都将损坏),在这种情况下你必须重新编译插件,并且将字段添加到结构中并不重要

,或者您希望与以前的插件进行二进制兼容,在这种情况下,请坚持使用纯数据和固定大小的字符数组(或者提供一个 API 来根据大小或传入 const 为字符串分配内存char* ),在这种情况下,结构中存在一些未使用的字段并非闻所未闻,然后在需要时将它们更改为有用的命名项。在这种情况下,结构中通常有一个字段来表示它代表哪个版本。

但期望二进制兼容性并使用 std::string 的情况很少见。您将永远无法升级或更改您的编译器。

Either your plugin is compiled with the same version of C++ compiler and std library source (or its std::string implementation may not be compatible, and all your string fields will break), in which case you have to recompile the plugins anyway, and adding fields to the struct won't matter

Or you want binary compatibility with previous plugins, in which case stick to plain data and fixed size char arrays ( or provide an API to allocate the memory for the strings based on size or passing in a const char* ), in which case it's not unheard of to have a few unused fields in the struct, and then change these to be usefully named items when the need arises. In such cases, it's also common to have a field in the struct to say which version it represents.

But it's very rare to expect binary compatibility and make use of std::string. You'll never be able to upgrade or change your compiler.

差↓一点笑了 2024-10-04 13:41:20

正如其他人所说,为了二进制兼容性,您很可能将自己限制为 C API。

Windows API 在许多地方通过将 size 成员放入结构中来维护二进制兼容性:

struct PluginInfo
{
    std::size_t size; // should be sizeof(PluginInfo)

    const char* s_Author;
    const char* s_Process;
    const char* s_ReleaseDate;
    //And so on...

    struct PluginVersion
    {
        const char* s_MajorVersion;
        const char* s_MinorVersion;
        //And so on...
    };
    PluginVersion o_Version;
};

当您创建这样的野兽时,您需要相应地设置 size 成员:

PluginInfo pluginInfo;
pluginInfo.size = sizeof(pluginInfo);
// set other members

当您针对较新版本的 API 编译代码,其中 struct 具有其他成员,其大小发生变化,并在其 size 成员中注明。 API 函数在传递这样的 struct 时,大概会首先读取其 size 成员,并分支到不同的方式来处理 struct,具体取决于它的大小。

当然,这假设进化是线性的,并且新数据始终只添加在struct的末尾。也就是说,您永远不会拥有具有相同大小的此类类型的不同版本。


然而,使用这样的野兽是确保用户在代码中引入错误的好方法。当他们针对新 API 重新编译代码时,sizeof(pluginInfo) 将自动调整,但不会自动设置其他成员。通过以 C 方式“初始化”struct 可以获得相当的安全性:

PluginInfo pluginInfo;
std::memset( &pluginInfo, 0, sizeof(pluginInfo) );
pluginInfo.size = sizeof(pluginInfo);

然而,即使抛开技术上的事实,将内存清零也可能不会为每个成员赋予合理的值(例如,可能是所有位设置为零的架构对于浮点类型来说不是有效值),这很烦人且容易出错,因为它需要三步构造。

一种解决方法是围绕该 C API 设计一个小型的内联 C++ 包装器。类似于:

class CPPPluginInfo : PluginInfo {
public:
  CPPPluginInfo()
   : PluginInfo() // initializes all values to 0
  {
    size = sizeof(PluginInfo);
  }

  CPPPluginInfo(const char* author /* other data */)
   : PluginInfo() // initializes all values to 0
  {
    size = sizeof(PluginInfo);
    s_Author = author;
    // set other data
  }
};

该类甚至可以负责将 C struct 成员指向的字符串存储在缓冲区中,以便该类的用户甚至不必担心这一点。


编辑:由于这似乎并不像我想象的那么清晰,所以这里有一个例子。
假设相同的 struct 将在更高版本的 API 中获得一些附加成员:

struct PluginInfo
{
    std::size_t size; // should be sizeof(PluginInfo)

    const char* s_Author;
    const char* s_Process;
    const char* s_ReleaseDate;
    //And so on...

    struct PluginVersion
    {
        const char* s_MajorVersion;
        const char* s_MinorVersion;
        //And so on...
    };
    PluginVersion o_Version;

    int fancy_API_version2_member;
};

当链接到旧版本 API 的插件现在初始化其 struct 时,如下

PluginInfo pluginInfo;
pluginInfo.size = sizeof(pluginInfo);
// set other members

所示struct 将是旧版本,缺少 API 版本 2 中新的闪亮数据成员。如果它现在调用第二个 API 接受指向 PluginInfo 的指针的函数,它将把旧 PluginInfo 的地址(短一个数据成员)传递给新 API 的函数。然而,对于版本 2 API 函数,pluginInfo->size 将小于 sizeof(PluginInfo),因此它将能够捕捉到这一点,并将指针视为指向没有 fancy_API_version2_member 的对象。 (据推测,在主机应用程序的 API 内部,PluginInfo 是带有 fancy_API_version2_member 的新且闪亮的 API,PluginInfoVersion1 是该 API 的新名称。因此,新 API 需要做的就是将插件传递的 PluginInfo* 转换为 PluginInfoVersion1* 并分支到可以处理的代码。 API

另一种方法是针对新版本 API 编译一个插件,其中 PluginInfo 包含 fancy_API_version2_member,插入旧版本的 对此一无所知的主机应用程序。同样,主机应用程序的 API 函数可以通过检查 pluginInfo->size 是否大于其自己的 PluginInfo sizeof 来捕获这一点。如果是这样,则该插件可能是针对比主机应用程序所知的更新版本的 API 进行编译的。 (或者插件写入无法正确初始化 size 成员。请参阅下文了解如何简化处理这种有点脆弱的方案。)
有两种方法可以解决这个问题:最简单的是拒绝加载插件。或者,如果可能的话,主机应用程序可以无论如何使用它,只需忽略它所传递的 PluginInfo 对象末尾的二进制内容,它不知道如何解释。
然而,后者很棘手,因为您需要在实现旧 API 时做出决定,而不确切知道新 API 是什么样子。

As was said by someone else, for binary compatibility you will most likely restrict yourself to a C API.

The Windows API in many places maintains binary compatibility by putting a size member into the struct:

struct PluginInfo
{
    std::size_t size; // should be sizeof(PluginInfo)

    const char* s_Author;
    const char* s_Process;
    const char* s_ReleaseDate;
    //And so on...

    struct PluginVersion
    {
        const char* s_MajorVersion;
        const char* s_MinorVersion;
        //And so on...
    };
    PluginVersion o_Version;
};

When you create such a beast, you need to set the size member accordingly:

PluginInfo pluginInfo;
pluginInfo.size = sizeof(pluginInfo);
// set other members

When you compile your code against a newer version of the API, where the struct has additional members, its size changes, and that is noted in its size member. The API functions, when being passed such a struct presumably will first read its size member and branch into different ways to handle the struct, depending on its size.

Of course, this assumes that evolution is linear and new data is always only added at the end of the struct. That is, you will never have different versions of such a type that have the same size.


However, using such a beast is a nice way of ensuring that user introduce errors into their code. When they re-compile their code against a new API, sizeof(pluginInfo) will automatically adapt, but the additional members won't be set automatically. A reasonably safety would be gained by "initializing" the struct the C way:

PluginInfo pluginInfo;
std::memset( &pluginInfo, 0, sizeof(pluginInfo) );
pluginInfo.size = sizeof(pluginInfo);

However, even putting aside the fact that, technically, zeroing memory might not put a reasonable value into each member (for example, there could be architectures where all bits set to zero is not a valid value for floating point types), this is annoying and error-prone because it requires three-step construction.

A way out would be to design a small and inlined C++ wrapper around that C API. Something like:

class CPPPluginInfo : PluginInfo {
public:
  CPPPluginInfo()
   : PluginInfo() // initializes all values to 0
  {
    size = sizeof(PluginInfo);
  }

  CPPPluginInfo(const char* author /* other data */)
   : PluginInfo() // initializes all values to 0
  {
    size = sizeof(PluginInfo);
    s_Author = author;
    // set other data
  }
};

The class could even take care of storing the strings pointed to by the C struct's members in a buffer, so that users of the class wouldn't even have to worry about that.


Edit: Since it seems this isn't as clear-cut as I thought it is, here's an example.
Suppose that very same struct will in a later version of the API get some additional member:

struct PluginInfo
{
    std::size_t size; // should be sizeof(PluginInfo)

    const char* s_Author;
    const char* s_Process;
    const char* s_ReleaseDate;
    //And so on...

    struct PluginVersion
    {
        const char* s_MajorVersion;
        const char* s_MinorVersion;
        //And so on...
    };
    PluginVersion o_Version;

    int fancy_API_version2_member;
};

When a plugin linked to the old version of the API now initializes its struct like this

PluginInfo pluginInfo;
pluginInfo.size = sizeof(pluginInfo);
// set other members

its struct will be the old version, missing the new and shiny data member from version 2 of the API. If it now calls a function of the second API accepting a pointer to PluginInfo, it will pass the address of an old PluginInfo, short one data member, to the new API's function. However, for the version 2 API function, pluginInfo->size will be smaller than sizeof(PluginInfo), so it will be able catch that, and treat the pointer as pointing to an object that doesn't have the fancy_API_version2_member. (Presumably, internal of the host app's API, PluginInfo is the new and shiny one with the fancy_API_version2_member, and PluginInfoVersion1 is the new name of the old type. So all the new API needs to do is to cast the PluginInfo* it got handed be the plugin into a PluginInfoVersion1* and branch off to code that can deal with that dusty old thing.)

The other way around would be a plugin compiled against the new version of the API, where PluginInfo contains the fancy_API_version2_member, plugged into an older version of the host app that knows nothing about it. Again, the host app's API functions can catch that by checking whether pluginInfo->size is greater than the sizeof their own PluginInfo. If so, the plugin presumably was compiled against a newer version of the API than the host app knows about. (Or the plugin write failed to properly initialize the size member. See below for how to simplify dealing with this somewhat brittle scheme.)
There's two ways to deal with that: The simplest is to just refuse to load the plugin. Or, if possible, the host app could work with this anyhow, simply ignoring the binary stuff at the end of the PluginInfo object it was passed which it doesn't know how to interpret.
However, the latter is tricky, since you need to decide this when you implement the old API, without knowing exactly what the new API will look like.

睫毛上残留的泪 2024-10-04 13:41:20

rwong 建议的 (std::map) 是一个很好的方向。这使得添加有意的字符串字段成为可能。如果您想要更大的灵活性,您可以声明一个抽象基类

class AbstractPluginInfoElement { public: virtual std::string toString() = 0;};

class StringPluginInfoElement : public AbstractPluginInfoElement 
{ 
  std::string m_value;
  public: 
   StringPluginInfoElement (std::string value) { m_value = value; }
   virtual std::string toString() { return m_value;}
};

然后您可以派生更复杂的类,例如 PluginVersion 等,并存储 map

what rwong suggest (std::map<std::string, std::string>) is a good direction. This is makes it possible to add deliberate string fields. If you want to have more flexibility you might declare an abstract base class

class AbstractPluginInfoElement { public: virtual std::string toString() = 0;};

and

class StringPluginInfoElement : public AbstractPluginInfoElement 
{ 
  std::string m_value;
  public: 
   StringPluginInfoElement (std::string value) { m_value = value; }
   virtual std::string toString() { return m_value;}
};

You might then derive more complex classes like PluginVersion etc. and store a map<std::string, AbstractPluginInfoElement*>.

旧人九事 2024-10-04 13:41:20

一个丑陋想法:

std::mapm_otherKeyValuePairs; 足够接下来的 500 年了。

编辑:

另一方面,此建议很容易被滥用,因此可能符合 TDWTF 的条件。

另一个同样可怕的想法:
a std::string m_everythingInAnXmlBlob;,如真实软件中所示。

(丑陋==不推荐)

编辑3:

  • 优点:
    std::map成员不受< a href="http://en.wikipedia.org/wiki/Object_slicing" rel="nofollow">对象切片。当旧的源代码复制属性包中包含新键的 PluginInfo 对象时,整个属性包都会被复制。
  • 缺点:
    许多程序员会开始向属性包添加不相关的东西,甚至开始编写这样的代码: 处理属性包中的值,导致维护噩梦。

One hideous idea:

A std::map<std::string, std::string> m_otherKeyValuePairs; would be enough for the next 500 years.

Edit:

On the other hand, this suggestion is so prone to misuse that it may qualify for a TDWTF.

Another equally hideous idea:
a std::string m_everythingInAnXmlBlob;, as seen in real software.

(hideous == not recommended)

Edit 3:

  • Advantage:
    The std::map member is not subject to object slicing. When older source code copies an PluginInfo object that contains new keys in the property bag, the entire property bag is copied.
  • Disadvantage:
    many programmers will start adding unrelated things to the property bag, and even starts writing code that processes the values in the property bag, leading to maintenance nightmare.
脸赞 2024-10-04 13:41:20

这是一个想法,不确定它是否适用于类,它肯定适用于结构:您可以使结构“保留”一些空间以供将来使用,如下所示:

struct Foo
{
  // Instance variables here.
  int bar;

  char _reserved[128]; // Make the class 128 bytes bigger.
}

初始化程序会在填充整个结构之前将其清零,因此该类的较新版本将访问现在位于“保留”区域内的字段,具有合理的默认值。

如果您仅在 _reserved 前面添加字段,相应地减小其大小,并且不修改/重新排列其他字段,那么应该没问题。不需要任何魔法。旧软件不会接触新字段,因为它们不了解这些字段,并且内存占用将保持不变。

Here's an idea, not sure whether it works with classes, it for sure works with structs: You can make the struct "reserve" some space to be used in the future like this:

struct Foo
{
  // Instance variables here.
  int bar;

  char _reserved[128]; // Make the class 128 bytes bigger.
}

An initializer would zero out whole struct before filling it, so newer versions of the class which would access fields that would now be within the "reserved" area are of sane default values.

If you only add fields in front of _reserved, reducing its size accordingly, and not modify/rearrange other fields you should be OK. No need for any magic. Older software will not touch the new fields as they don't know about them, and the memory footprint will remain the same.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文