Curiously Recurring Template Pattern (CRTP) 是正确的解决方案吗?
Is the Curiously Recurring Template Pattern (CRTP) the right solution here?
场景
考虑一个 class Logger
,它有一个为标准 C++ 类型重载的成员函数 write()
,还有一些方便的函数模板,如 writeLine()
,它在内部调用write()
:
class Logger {
public:
void write(int x) { ... }
void write(double x) { ... }
...
template <typename T>
void writeLine(T x) { write(x); ... }
...
};
进一步考虑一个 subclass FooLogger
,它为特定于域的类型添加了额外的 write()
重载(我们称其中两个为 FooType1
和 FooType2
):
class FooLogger : public Logger {
public:
using Logger::write;
void write(FooType1 x) { ... }
void write(FooType2 x) { ... }
...
};
(self-contained example program at Ideone)
问题
FooLogger::write()
,当直接调用时,现在支持两个 classes 中的 either 提供重载的任何参数。
但是,FooLogger::writeLine()
仅支持 class Logger
具有 write()
重载的参数类型...它看不到额外的 write()
在 class FooLogger
.
中声明的重载
我希望它能看到它们,这样它也可以用这些参数类型调用!
当前解决方案
我使用 Curiously Recurring Template Pattern (CRTP) 让它工作:
template <typename TDerivedClass>
class AbstractLogger {
...
template <typename T>
void writeLine(T x) { static_cast<TDerivedClass*>(this)->write(x); ... }
};
class Logger : AbstractLogger {}
class FooLogger : public AbstractLogger<FooLogger> {
...
};
(self-contained example program at Ideone)
虽然它完成了工作,但代价是增加了代码的复杂性和冗长:
- 它使基础 class 的实现明显更难阅读(参见 Ideone link),更难维护(一定不要忘记做
static_cast
舞蹈将来在适当的时候向 class 添加更多代码!)
- 需要将
AbstractLogger
和 Logger
分成两个 class。
- 因为 base-class 现在是一个 class 模板,它所有成员函数的实现现在必须包含在头文件中(而不是
.cpp
文件)-即使是那些不需要做 static_cast
事情的人。
问题
考虑到以上情况,我正在寻求具有 C++ 经验的人的见解:
- CRTP 是这项工作的正确工具吗?
- 还有其他方法可以解决这个问题吗?
为什么不使用在您的类型和记录器的流输出类型上定义的自由函数,例如 operator<<
,或者只使用在可见时调用的函数?有关如何执行此操作的示例:编写 googletest 以便您可以通过提供序列化方法以这种方式自定义所有断言。请参阅 Teaching Googletest How To Print Your Values 然后您可以查看实现以了解他们是如何做到的。
(请注意,googletest 有太多方法:您可以在 class 中提供 PrintTo()
方法,或者您可以重载 operator<<
,如果两者都满足,则首选 PrintTo()
可用。这样做的好处是,您可以 以不同于 的方式序列化为日志记录,而不是序列化为典型的输出流(例如,您的 class 已经有一个 operator<<
'为日志做你想做的事)。
(魔法全部包含在gtest-printer.h - see class UniversalPrinter
at line 685中用于触发。)
这还有一个优点,即很容易添加任何 class/struct/object 以正确记录,甚至无需费心扩展日志记录 class。此外......如果有人扩展记录器class(即从它派生)以序列化class AAA
,并且在不同的代码段中有不同的推导序列化会发生什么class BBB
然后最后你写了一些你想记录 AAA
s 和 BBB
s 的代码?派生的 class 方法在那里效果不佳 ...
反之亦然:
template <typename ...Ts>
class Logger : private Ts...
{
public:
using Ts::write...;
void write(int x) { /*...*/ }
void write(double x) { /*...*/ }
// ...
template <typename T>
void writeLine(T x) { write(x); /*...*/ }
// ...
};
class FooWriter
{
public:
void write(FooType1 x) { /*...*/ }
void write(FooType2 x) { /*...*/ }
};
using FooLogger = Logger<FooWriter>;
然后使用任何(或他们的别名):
Logger<>
或 Logger<FooWriter>
或 Logger<FooWriter, BarWriter>
...
场景
考虑一个 class Logger
,它有一个为标准 C++ 类型重载的成员函数 write()
,还有一些方便的函数模板,如 writeLine()
,它在内部调用write()
:
class Logger {
public:
void write(int x) { ... }
void write(double x) { ... }
...
template <typename T>
void writeLine(T x) { write(x); ... }
...
};
进一步考虑一个 subclass FooLogger
,它为特定于域的类型添加了额外的 write()
重载(我们称其中两个为 FooType1
和 FooType2
):
class FooLogger : public Logger {
public:
using Logger::write;
void write(FooType1 x) { ... }
void write(FooType2 x) { ... }
...
};
(self-contained example program at Ideone)
问题
FooLogger::write()
,当直接调用时,现在支持两个 classes 中的 either 提供重载的任何参数。
但是,FooLogger::writeLine()
仅支持 class Logger
具有 write()
重载的参数类型...它看不到额外的 write()
在 class FooLogger
.
我希望它能看到它们,这样它也可以用这些参数类型调用!
当前解决方案
我使用 Curiously Recurring Template Pattern (CRTP) 让它工作:
template <typename TDerivedClass>
class AbstractLogger {
...
template <typename T>
void writeLine(T x) { static_cast<TDerivedClass*>(this)->write(x); ... }
};
class Logger : AbstractLogger {}
class FooLogger : public AbstractLogger<FooLogger> {
...
};
(self-contained example program at Ideone)
虽然它完成了工作,但代价是增加了代码的复杂性和冗长:
- 它使基础 class 的实现明显更难阅读(参见 Ideone link),更难维护(一定不要忘记做
static_cast
舞蹈将来在适当的时候向 class 添加更多代码!) - 需要将
AbstractLogger
和Logger
分成两个 class。 - 因为 base-class 现在是一个 class 模板,它所有成员函数的实现现在必须包含在头文件中(而不是
.cpp
文件)-即使是那些不需要做static_cast
事情的人。
问题
考虑到以上情况,我正在寻求具有 C++ 经验的人的见解:
- CRTP 是这项工作的正确工具吗?
- 还有其他方法可以解决这个问题吗?
为什么不使用在您的类型和记录器的流输出类型上定义的自由函数,例如 operator<<
,或者只使用在可见时调用的函数?有关如何执行此操作的示例:编写 googletest 以便您可以通过提供序列化方法以这种方式自定义所有断言。请参阅 Teaching Googletest How To Print Your Values 然后您可以查看实现以了解他们是如何做到的。
(请注意,googletest 有太多方法:您可以在 class 中提供 PrintTo()
方法,或者您可以重载 operator<<
,如果两者都满足,则首选 PrintTo()
可用。这样做的好处是,您可以 以不同于 的方式序列化为日志记录,而不是序列化为典型的输出流(例如,您的 class 已经有一个 operator<<
'为日志做你想做的事)。
(魔法全部包含在gtest-printer.h - see class UniversalPrinter
at line 685中用于触发。)
这还有一个优点,即很容易添加任何 class/struct/object 以正确记录,甚至无需费心扩展日志记录 class。此外......如果有人扩展记录器class(即从它派生)以序列化class AAA
,并且在不同的代码段中有不同的推导序列化会发生什么class BBB
然后最后你写了一些你想记录 AAA
s 和 BBB
s 的代码?派生的 class 方法在那里效果不佳 ...
反之亦然:
template <typename ...Ts>
class Logger : private Ts...
{
public:
using Ts::write...;
void write(int x) { /*...*/ }
void write(double x) { /*...*/ }
// ...
template <typename T>
void writeLine(T x) { write(x); /*...*/ }
// ...
};
class FooWriter
{
public:
void write(FooType1 x) { /*...*/ }
void write(FooType2 x) { /*...*/ }
};
using FooLogger = Logger<FooWriter>;
然后使用任何(或他们的别名):
Logger<>
或 Logger<FooWriter>
或 Logger<FooWriter, BarWriter>
...