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

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

有人可以描述作者 John Lakos 在以下引用中提到的精确编码策略吗?

约翰·拉科斯:

More controversially, it is often better to have two copies of a struct—e.g., one nested/private in the .h file (accessible to inline methods and friends) and the other at file scope in the .cpp file (accessible to file-scope static functions) —and make sure to keep them in sync locally than to pollute the global space with an implementation detail.

引用出现在较新的 Lakos 大部头中,Large Scale C++

(这本书是对与 Lakos 早期著作相同主题的更新处理,Large Scale C++ Software Design

引用在第 9 页第 0.2 节中。

如果后面的章节讲清楚 Lakos 描述的内容,我会 return 在这里 post 一个答案。

与此同时,我着迷于理解这句话,我试图扫描本书的 table 内容和索引以寻找更多线索,但尚未找到答案。

这是我自己的示例代码,用于解决我 想象 会被神秘策略解决的问题:

// 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,原因我很清楚。

将上述代码与书中的引用联系起来

回到 Lakos 的引用,我的想法是 FileScopeComputation 是引用所称的 “文件范围静态函数”.[= 的一个实例34=]

使代码编译的一个“明显”解决方案是移动 InnerComponent,以便它在 OuterComponentpublic 部分声明。

但是,我认为我的“显而易见”的解决方案是有罪的(根据引文)[污染] 全局 space 的实现细节。

因此,我的代码似乎同时捕获了:(a) 所提及策略的目标和 (b) 一种潜在解决方案的不必要污染。那么替代解决方案是什么?

请回答其中一个或两个:

(1) 有没有办法在 cpp 文件中制作另一个 struct InnerComponent 的副本,使得字段 OuterComponent::m_inner 保持私有,类型 OuterComponent::InnerComponent 也保持私有,并且然而不知何故,函数 FileScopeComputation 有某种方法可以做一些“等同于”访问 InnerComponent?

实例上的数据的事情

我试过一些奇怪的选角方法,但没有什么看起来值得在书中推荐的。同时,根据我的经验,Lakos在书上推荐的东西都非常值得推荐。

(2) 我是否完全误读了引用适用于哪种场景?如果是这样,那么引用实际上指的是什么 C++ 软件设计问题?还有什么其他问题涉及 “一个结构的两个副本...一个在 h 文件中...另一个在 cpp 文件中”?

更新:

的基础上,上面的代码确实可以通过最小的改动编译,这样:

头代码多了一行代码:

...
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 实际上可能指的是 public API 委托调用的单独命名的类型。引用与 Lakos Factoring pattern(“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) 与 public-facing API (Widget) 分开的方法,同时也便于测试:

  • WidgetUtil
  • 的单元测试中单独测试的实现细节
  • Widget 的测试无需担心 WidgetUtil 成员中的 side-effects,因为后者可以在 [=12] 中使用静态依赖注入完全模拟=](使其成为 class 模板)。

如果我们回顾一下 Lakos 的引述(在 OP 中),WidgetUtil 可以像 file-scope class 一样放置在 widget.cpp源文件,隐藏远离publicAPI。这将意味着更多的封装,但不会像上面描述的分离那样方便测试。

最后,请注意,使 Widget 成为 class 模板并不一定意味着用定义(需要在每个 TU 中编译,包括和实例化 Widget)。由于 Widget 只是 class 模板 以方便测试,它的产品实现将永远只使用 一个实例化 ,即注入产品WidgetUtil。这意味着可以将 Widget class 模板成员函数的定义与其声明分开,就像 non-template classes 一样,并显式实例化 Widget<WidgetUtil>专业化 widget.cpp。例如。使用以下方法:

  • 分离成员函数的定义 class 模板 Widget 来自 class 模板的 header 文件 定义,到单独的 -timpl.h header 文件,
  • -timpl.h header 文件依次包含在关联的 .cpp 文件中,该文件又包含 Widget class 的显式实例化生产中使用的单一类型模板参数的模板,即 WidgetUtil.

测试可以类似地包含 -timpl.h header 并使用模拟 util class 实例化 Widget class 模板。