给定一个 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
,以便它在 OuterComponent
的 public
部分声明。
但是,我认为我的“显而易见”的解决方案是有罪的(根据引文)[污染] 全局 space 的实现细节。
因此,我的代码似乎同时捕获了:(a) 所提及策略的目标和 (b) 一种潜在解决方案的不必要污染。那么替代解决方案是什么?
请回答其中一个或两个:
(1) 有没有办法在 cpp 文件中制作另一个 struct InnerComponent
的副本,使得字段 OuterComponent::m_inner
保持私有,类型 OuterComponent::InnerComponent
也保持私有,并且然而不知何故,函数 FileScopeComputation
有某种方法可以做一些“等同于”访问 InnerComponent
?
实例上的数据的事情
我试过一些奇怪的选角方法,但没有什么看起来值得在书中推荐的。同时,根据我的经验,Lakos在书上推荐的东西都非常值得推荐。
(2) 我是否完全误读了引用适用于哪种场景?如果是这样,那么引用实际上指的是什么 C++ 软件设计问题?还有什么其他问题涉及 “一个结构的两个副本...一个在 h 文件中...另一个在 cpp 文件中”?
更新:
在的基础上,上面的代码确实可以通过最小的改动编译,这样:
- 字段
OuterComponent::m_inner
保持私有
- 类型
OuterComponent::InnerComponent
也保持私有
- 然而函数
FileScopeComputation
有一些方法可以访问 InnerComponent
实例上的数据
头代码多了一行代码:
...
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 模板。
有人可以描述作者 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
,以便它在 OuterComponent
的 public
部分声明。
但是,我认为我的“显而易见”的解决方案是有罪的(根据引文)[污染] 全局 space 的实现细节。
因此,我的代码似乎同时捕获了:(a) 所提及策略的目标和 (b) 一种潜在解决方案的不必要污染。那么替代解决方案是什么?
请回答其中一个或两个:
(1) 有没有办法在 cpp 文件中制作另一个 struct InnerComponent
的副本,使得字段 OuterComponent::m_inner
保持私有,类型 OuterComponent::InnerComponent
也保持私有,并且然而不知何故,函数 FileScopeComputation
有某种方法可以做一些“等同于”访问 InnerComponent
?
我试过一些奇怪的选角方法,但没有什么看起来值得在书中推荐的。同时,根据我的经验,Lakos在书上推荐的东西都非常值得推荐。
(2) 我是否完全误读了引用适用于哪种场景?如果是这样,那么引用实际上指的是什么 C++ 软件设计问题?还有什么其他问题涉及 “一个结构的两个副本...一个在 h 文件中...另一个在 cpp 文件中”?
更新:
在
- 字段
OuterComponent::m_inner
保持私有 - 类型
OuterComponent::InnerComponent
也保持私有 - 然而函数
FileScopeComputation
有一些方法可以访问InnerComponent
实例上的数据
头代码多了一行代码:
...
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 模板。