减少 C++ 中的代码重复:在略有不同的项目中使用相同的样板代码片段
Reducing code duplication in C++: using same boilerplate snippets across slightly varying projects
关于我的具体用例:我有一个旨在与 Unreal Engine 项目集成的插件,为了向插件用户演示如何执行此操作,我将其集成以 Unreal 的一款免费示例游戏为例。集成非常针对游戏,因为它可以修改菜单等操作,让用户可以轻松地与我的插件进行交互。
然而,在理想的世界中,我希望能够:
- 跨多个不同的 Unreal Engine 版本提供与示例游戏的集成。至少这将包括 3 个当前存在的 Unreal 版本(4.24、4.25 和 4.26),但可能会扩展到 N 个不同的未来版本。这实质上使集成代码成为“样板”,因为它是每个示例游戏版本中功能所必需的,但在不同版本之间根本没有变化。
- 能够从一处维护大部分集成代码。我不想每次更改某些内容时都必须在每个示例游戏集成中进行相同的修改,因为像这样处理多个并行代码库需要大量工作并且会增加错误的可能性。
这几乎是一个可以通过代码补丁解决的问题:无论我使用的是哪个版本的示例游戏,集成代码都适合相同文件中的相同 functions/classes。但是,示例游戏文件本身的内容在不同引擎版本中 并不完全相同,因此说明“在此行将此大块插入此文件”的补丁并不总是修正它。理论上也有可能在未来的示例游戏中引入更实质性的变化,这可能需要我在这种情况下更改我的集成(尽管这还没有发生——在次要引擎版本中变化似乎很小)。
解决这个问题的最佳方法是什么?我能想到的一种特别可怕的方法(但证明了这个概念的方法)是将集成的每个块分离到一个单独的文件中,然后直接 #include "Chunk1.inc"
、#include "Chunk2.inc"
、...
进入示例游戏每个版本中的相关 classes 和函数。
void ExistingClass::PerformOperationA()
{
// ... existing implementation ...
// Horrible hack:
#include "IntegrationChunk1.inc"
}
我确定这会编译,但我编写或最终用户阅读起来都不容易,因为代码本身在像这样的片段中隔离时会缺少所有上下文。
另一个示例可能是将集成代码的每个部分保存在一个单独的 class 中,该 class 被声明为代码通常所在的 class 的友元。 friend class 被赋予指向 main class 的指针,因此可以访问任何所需的私有成员,并具有在适当点调用的相关函数。这种模仿 C# 的 partial
class 概念,并且会使原始代码和修改后的代码之间的关系变得明确,因为它们都是纯粹、透明的 C++。
例如:
class ExistingClass
{
// Additional declaration of the friend integration class:
friend class ExistingClass_Integration;
ExistingClass_Integration m_Integration;
public:
ExistingClass() :
// ... existing initialisers ...
m_Integration(this)
void PerformOperationA()
{
// ... existing code ...
// Additional bits that need to happen as part of the integration:
m_Integration.PerformOperationA();
}
};
class ExistingClass_Integration
{
ExistingClass* m_Owner = nullptr;
public:
ExistingClass_Integration(ExistingClass* owner) :
m_Owner(owner)
{
}
void PerformOperationA()
{
// Do some things here, potentially using m_Owner->...
}
};
除了关于这是否是推荐做法的问题,这里还有一个可读性权衡。尽管集成代码修改的内容定义明确且易于遵循,但它并不能真正代表真实用户将如何集成到他们的游戏中——他们只会直接编写代码。额外的表述只是为了我自己的维护,这可能会使集成看起来比实际需要的更复杂。
这是思考这个问题的正确方法,还是我需要退后一步尝试不同的方法?有人有推荐吗?
本着更具体地说明我实际使用的代码的精神,我的集成包括以下内容:
现有源文件中的额外包含
#include "Engine/GameInstance.h"
#include "Engine/NetworkDelegates.h"
#include "MyPlugin/MyClass.h" // <- NEW
现有 classes
的扩充
class ExistingClass
{
public:
void Function1();
void Function2();
private:
int m_Var1;
int m_Var2;
// BEGIN INTEGRATION
public:
void ExtraFunction();
private:
int m_ExtraVar;
// END INTEGRATION
};
连接到现有函数
此处的集成旨在简单地调用一个新函数和 return,以尽量减少对现有代码的差异。
void ExistingClass::CreateMainMenu()
{
// Existing implementation, eg.:
MainMenuUI = MakeShareable(new FShooterMainMenu());
MainMenuUI->Construct(this, Player);
MainMenuUI->AddMenuToGameViewport();
SetUpExtrasAfterMenuCreated() // <- NEW
// ... further existing implementation ...
}
juggling multiple parallel codebases like this is a lot of work and increases the probability of bugs. .. What is the best way to attack this problem?
没有最好的办法。通用补丁需要手动工作,并且在一些公司中有专职员工专门从事此工作。这就是为什么 任何 产品(软件或其他任何东西)的多个受支持版本需要很多钱。
最好的方法是以最小化支持旧版本的成本的方式编写软件。通常,这意味着将测试和验证旧版本的成本降到最低,而不是进行在许多情况下根本不可能的自动修补。有时,如果可以修改基本代码以使其尽可能容易地打补丁,那么运气可能会更好,但您的情况似乎并非如此。
即使某些案例子集可以自动化,有时出于多种原因这样做甚至没有意义。其中一些您已经声明:它可能无法在未来的版本中工作,可能无法保证可靠,用户不期望这样的代码等。
TL;DR:向后移植和维护多个软件分支并不便宜。
关于我的具体用例:我有一个旨在与 Unreal Engine 项目集成的插件,为了向插件用户演示如何执行此操作,我将其集成以 Unreal 的一款免费示例游戏为例。集成非常针对游戏,因为它可以修改菜单等操作,让用户可以轻松地与我的插件进行交互。
然而,在理想的世界中,我希望能够:
- 跨多个不同的 Unreal Engine 版本提供与示例游戏的集成。至少这将包括 3 个当前存在的 Unreal 版本(4.24、4.25 和 4.26),但可能会扩展到 N 个不同的未来版本。这实质上使集成代码成为“样板”,因为它是每个示例游戏版本中功能所必需的,但在不同版本之间根本没有变化。
- 能够从一处维护大部分集成代码。我不想每次更改某些内容时都必须在每个示例游戏集成中进行相同的修改,因为像这样处理多个并行代码库需要大量工作并且会增加错误的可能性。
这几乎是一个可以通过代码补丁解决的问题:无论我使用的是哪个版本的示例游戏,集成代码都适合相同文件中的相同 functions/classes。但是,示例游戏文件本身的内容在不同引擎版本中 并不完全相同,因此说明“在此行将此大块插入此文件”的补丁并不总是修正它。理论上也有可能在未来的示例游戏中引入更实质性的变化,这可能需要我在这种情况下更改我的集成(尽管这还没有发生——在次要引擎版本中变化似乎很小)。
解决这个问题的最佳方法是什么?我能想到的一种特别可怕的方法(但证明了这个概念的方法)是将集成的每个块分离到一个单独的文件中,然后直接 #include "Chunk1.inc"
、#include "Chunk2.inc"
、...
进入示例游戏每个版本中的相关 classes 和函数。
void ExistingClass::PerformOperationA()
{
// ... existing implementation ...
// Horrible hack:
#include "IntegrationChunk1.inc"
}
我确定这会编译,但我编写或最终用户阅读起来都不容易,因为代码本身在像这样的片段中隔离时会缺少所有上下文。
另一个示例可能是将集成代码的每个部分保存在一个单独的 class 中,该 class 被声明为代码通常所在的 class 的友元。 friend class 被赋予指向 main class 的指针,因此可以访问任何所需的私有成员,并具有在适当点调用的相关函数。这种模仿 C# 的 partial
class 概念,并且会使原始代码和修改后的代码之间的关系变得明确,因为它们都是纯粹、透明的 C++。
例如:
class ExistingClass
{
// Additional declaration of the friend integration class:
friend class ExistingClass_Integration;
ExistingClass_Integration m_Integration;
public:
ExistingClass() :
// ... existing initialisers ...
m_Integration(this)
void PerformOperationA()
{
// ... existing code ...
// Additional bits that need to happen as part of the integration:
m_Integration.PerformOperationA();
}
};
class ExistingClass_Integration
{
ExistingClass* m_Owner = nullptr;
public:
ExistingClass_Integration(ExistingClass* owner) :
m_Owner(owner)
{
}
void PerformOperationA()
{
// Do some things here, potentially using m_Owner->...
}
};
除了关于这是否是推荐做法的问题,这里还有一个可读性权衡。尽管集成代码修改的内容定义明确且易于遵循,但它并不能真正代表真实用户将如何集成到他们的游戏中——他们只会直接编写代码。额外的表述只是为了我自己的维护,这可能会使集成看起来比实际需要的更复杂。
这是思考这个问题的正确方法,还是我需要退后一步尝试不同的方法?有人有推荐吗?
本着更具体地说明我实际使用的代码的精神,我的集成包括以下内容:
现有源文件中的额外包含
#include "Engine/GameInstance.h"
#include "Engine/NetworkDelegates.h"
#include "MyPlugin/MyClass.h" // <- NEW
现有 classes
的扩充class ExistingClass
{
public:
void Function1();
void Function2();
private:
int m_Var1;
int m_Var2;
// BEGIN INTEGRATION
public:
void ExtraFunction();
private:
int m_ExtraVar;
// END INTEGRATION
};
连接到现有函数
此处的集成旨在简单地调用一个新函数和 return,以尽量减少对现有代码的差异。
void ExistingClass::CreateMainMenu()
{
// Existing implementation, eg.:
MainMenuUI = MakeShareable(new FShooterMainMenu());
MainMenuUI->Construct(this, Player);
MainMenuUI->AddMenuToGameViewport();
SetUpExtrasAfterMenuCreated() // <- NEW
// ... further existing implementation ...
}
juggling multiple parallel codebases like this is a lot of work and increases the probability of bugs. .. What is the best way to attack this problem?
没有最好的办法。通用补丁需要手动工作,并且在一些公司中有专职员工专门从事此工作。这就是为什么 任何 产品(软件或其他任何东西)的多个受支持版本需要很多钱。
最好的方法是以最小化支持旧版本的成本的方式编写软件。通常,这意味着将测试和验证旧版本的成本降到最低,而不是进行在许多情况下根本不可能的自动修补。有时,如果可以修改基本代码以使其尽可能容易地打补丁,那么运气可能会更好,但您的情况似乎并非如此。
即使某些案例子集可以自动化,有时出于多种原因这样做甚至没有意义。其中一些您已经声明:它可能无法在未来的版本中工作,可能无法保证可靠,用户不期望这样的代码等。
TL;DR:向后移植和维护多个软件分支并不便宜。