给定一个C++嵌套的私有结构类型,是否有从文件范围静态函数访问它的策略

Given a C++ nested private struct type, are there tactics for accessing it from a file scope static function?

本文关键字:文件 是否 范围 静态函数 策略 访问 类型 结构 一个 C++ 嵌套      更新时间:2023-10-16

有人能描述一下作者John Lakos在下面引用的精确编码策略吗?

约翰·拉科斯:

更具争议的是,通常最好有一个结构的两个副本——例如,一个嵌套/私有在.h文件中(内联方法和朋友可以访问),另一个在.cpp文件中的文件范围内(文件范围静态函数可以访问)——并确保它们在本地保持同步,而不是用实现细节污染全局空间。

这句话出现在较新的Lakos大部头《大规模C++》中。

(这本书是对与Lakos早期著作《大规模C++软件设计》相同主题的更新处理)

报价见第9页第0.2节。

如果后面的章节能弄清楚拉科斯在描述什么,我会回到这里并给出答案。

与此同时,我开始着迷于理解这句话,我试图扫描这本书的目录和索引以寻找进一步的线索,但尚未得到答案。

以下是我自己的示例代码,用于我想象将通过神秘策略解决的问题:

// THE HEADER
namespace project
{
class OuterComponent
{
public:
inline int GetFoo()
{
return m_inner.foo;
}
int GetBar();
private:
struct InnerComponent
{
int foo = 0;
int bar = 0;
};
InnerComponent m_inner;
};
} // namespace project

连同:

// THE CPP IMPLEMENTATION FILE
namespace project
{
namespace
{
/*
MYSTERY:
Per the book quotation, I can somehow add a "copy of InnerComponent" here?
And "make sure to keep them in sync locally"?
*/
// COMPILATION ERROR (see below)
int FileScopeComputation( OuterComponent::InnerComponent i )
{
return i.bar - 3;
}
} // namespace
int OuterComponent::GetBar()
{
return FileScopeComputation( m_inner );
}
} // namespace project

当然,以上内容不会编译。

错误将类似于:

error: ‘struct project::OuterComponent::InnerComponent’ is private within this context
int FileScopeComputation( OuterComponent::InnerComponent i )
^~~~~~~~~~~~~~

名为FileScopeComputation的免费函数无法访问InnerComponent,原因我很清楚。

将上述代码与图书报价关联

回到拉科斯的名言,我认为FileScopeComputation是该名言所称的"文件范围静态函数">

一个";明显的";编译代码的解决方案是移动InnerComponent,使其在OuterComponentpublic部分中声明。

然而,我把我的";明显的";有罪的解决方案(根据报价)";通过实施细节[污染]全球空间">

因此,我的代码似乎捕捉到了两者:(a)间接策略的目标和(b)一个潜在解决方案的不必要污染。那么,替代方案是什么呢?

请回答其中一个或两个:

(1) 是否有一种方法可以在cpp文件中制作struct InnerComponent的另一个副本,使得字段OuterComponent::m_inner保持私有,类型OuterComponent::InnerComponent也保持私有,并且函数FileScopeComputation不知何故有某种方法可以做一些事情";等效";访问InnerComponent实例上的数据?

我试过一些奇怪的选角方法,但没有什么值得在书中推荐的。同时,根据我的经验,拉科斯在书中推荐的东西都很值得推荐。

(2) 我是不是完全误读了这句话适用于什么样的场景?如果是这样的话,那么这句话实际上指的是什么C++软件设计问题?还有什么问题涉及";一个结构的两个副本。。。h文件中的一个。。。。cpp文件中的另一个"?

更新:

基于dfri敏锐的回答,上述代码确实可以在最小的更改下进行编译,并且:

  • 字段OuterComponent::m_inner保持私有
  • 类型CCD_ 13也保持私有
  • 而函数CCD_ 14具有访问CCD_

标题代码获得一行额外的代码:

...
private:
struct InnerUtil; // <-- this line was ADDED. all else is same as above.
struct InnerComponent
{
int foo = 0;
int bar = 0;
};
InnerComponent m_inner;
};

cpp文件代码变为:

struct OuterComponent::InnerUtil
{
static int FileScopeComputation( OuterComponent::InnerComponent i )
{
return i.bar - 3;
}
};
int OuterComponent::GetBar()
{
return InnerUtil::FileScopeComputation( m_inner );
}

因子分解模式(Lakos)

Lakos可能实际上是指公共API委托调用的单独命名的类型。在引用和Lakos Factoring模式("Imp和ImpUtil模式")之间有一种相似的感觉;ImpUtil";部分

struct A {};
struct B {};
struct C {};
// widgetutil.h
// (definitions placed in widgetutil.cpp)
struct WidgetUtil {
// "Keep API in sync with Widget::foo".
static void foo(const A& b, const B& c, const C& a) {
// All implementation here in the util.
(void)a; (void)b; (void)c;
}

// "Keep API in sync with Widget::bar".
static void bar(const B& b, const C& c) {
// All implementation here in the util.
(void)b; (void)c;
}
};
// widget.h
// includes "widgetutil.h"
// Public-facing API
// (Ignoring the Imp pattern, only using the Util pattern).
struct Widget {
void foo(const A& a, const B& b) const {
// Only delegation to the util.
WidgetUtil::foo(a, b, c_);
}
void bar(const B& b) const {
// Only delegation to the util.
WidgetUtil::bar(b, c_);
}
private:
C c_{};
};
int main() {
const Widget w;
w.foo(A{}, B{});  // --> WidgetUtil::foo
}

这是一种将实现细节(WidgetUtil)与公开的API(Widget)分离的方法,也有助于测试:

  • 实现详细介绍了CCD_ 18单元测试中隔离测试的单元
  • 可以在不必担心WidgetUtil成员中的副作用的情况下执行Widget的测试,因为后者可以在Widget中使用静态依赖注入来完全模拟(使其成为类模板)

如果我们回顾Lakos的报价(在OP中),WidgetUtil也可以作为文件-操作类放置在widget.cpp源文件中,隐藏在公共API之外。这将意味着更多的封装,但不会像上述分离那样促进测试。

最后,请注意,使Widget成为类模板并不一定意味着用定义(需要在包括和实例化Widget的每个TU中编译)污染widget.h的用户的翻译单元。由于Widget仅作为类模板以便于测试,因此它的产品实现将只使用单个实例化,即注入产品WidgetUtil。这意味着可以将Widget类模板成员函数的定义与其声明分离,就像非模板类一样,并在widget.cpp中显式实例化Widget<WidgetUtil>专用化。例如,使用以下方法:

  • 分离类模板的头文件中的类模板Widget定义转换为单独的CCD_
  • -timpl.h头文件又包含在相关联的.cpp文件中,该文件又包含用于生产中使用的单个类型模板参数(即WidgetUtil)的Widget类模板的显式实例化

测试可以类似地包括-timpl.h头,并使用模拟的util类实例化Widget类模板。