在派生类中专用化基的模板化成员函数
Specialize the base's templated member function in derived class
TL;DR;
编译以下代码以两个未解析的外部代码结束。
问题
有没有办法在基类中有一个未定义的模板化成员函数,并使该函数部分专用于派生类,以便部分专用化将限制为定义它的派生类?
解释
如您所见,serial_port
和 liquid_crystal
都派生自stream
基类。stream
类将提供一个统一的接口来将文本发送到不同的外围设备。从stream
派生的每个类都必须实现print(char)
函数,该函数将处理与外设的低级通信。除此之外,还有一个未定义的模板化打印版本,它可以专门用于用户可能想要打印的任何自定义类型。
stream
类具有operator <<
的模板化定义,用于将数据写入流。此运算符将调用将处理实际打印的stream::print
。如您所见,已经定义了print(const char*)
和print(fill)
,因为我希望它们存在于 stream
的每个派生类中。
现在这是引入错误的部分
我想与之通信的外围设备有一些基本命令(LCD:将光标移动到x,y坐标,串行端口:将波特率设置为x(,它们之间不可互换,这意味着LCD不知道如何更改波特率,串行端口没有可以移动到特定坐标的光标。我想通过operator <<
传递命令,就像我对fill
所做的那样。每个命令都是一个新的结构,保存命令所需的数据,并且将有一个专门的print
版本来处理每个命令。
这在理论上有效,但在编译过程中失败,因为print
的专用版本是在派生类中定义的,但operator <<
是在stream
类中定义的。当我将命令传递给流时,链接器会在stream
内部查找print
的专用定义,当然它会失败,因为这些定义根本不存在。
我怎样才能克服这个错误?我使用的是Visual Studio 15 Preview 4,我没有更改任何编译器标志。
源代码
#include <iostream>
class stream
{
public:
struct fill
{
int n;
char ch;
};
stream()
{}
virtual ~stream()
{}
template <typename T>
stream& operator << (T t)
{
this->print(t);
return *this;
}
protected:
virtual void print(char) = 0;
template <typename T>
void print(T);
};
template <>
void stream::print<const char*>(const char* str)
{
while (*str != ' ')
this->print(*(str++));
}
template <>
void stream::print<stream::fill>(stream::fill f)
{
while (f.n > 0)
{
this->print(f.ch);
f.n--;
}
}
class serial_port : public stream
{
public:
struct set_baudrate
{
int baud;
};
using stream::stream;
private:
void print(char c) override
{
// TODO: print to the actual serial port
std::cout << c;
}
template <typename T>
void print(T t)
{
stream::print<T>(t);
}
};
template <>
void serial_port::print<serial_port::set_baudrate>(serial_port::set_baudrate)
{
this->print("set_baudrate");
}
class liquid_crystal : public stream
{
public:
struct move
{
int x;
int y;
};
using stream::stream;
private:
void print(char c) override
{
// TODO: print to a character LCD
std::cout << c;
}
template <typename T>
void print(T t)
{
stream::print<T>(t);
}
};
template <>
void liquid_crystal::print<liquid_crystal::move>(liquid_crystal::move)
{
this->print("move");
}
int main()
{
liquid_crystal lcd;
lcd << liquid_crystal::move{ 1, 2 };
serial_port serial;
serial << serial_port::set_baudrate{ 9600 };
}
编辑
问题在编译器输出上更明显,链接器正在寻找void stream::print<liquid_crystal::move>(liquid_crystal::move)
和void stream::print<serial_port::set_baudrate>(serial_port::set_baudrate)
但函数签名应该void liquid_crystal::print<liquid_crystal::move>(liquid_crystal::move)
和void serial_port::print<serial_port::set_baudrate>(serial_port::set_baudrate)
。
在main
中,行:
lcd << liquid_crystal::move{ 1, 2 };
调用:
stream::operator<< <liquid_crystal::move>(liquid_crystal::move)
然后调用:
stream::print<liquid_crystal::move>(liquid_crystal::move)
但 stream::print
函数仅针对const char*
和stream::fill
类型定义。
不使用 liquid_crystal::print
函数,因为它不是stream::print
的覆盖(它在类liquid_crystal
隐藏stream::print
(。为了使用this
(这是一个stream*
,而不是一个liquid_crystal*
(从stream
访问它,stream::print
必须是虚拟的。但在这种情况下,这是不可能的,因为它是一个模板函数。
一般来说,解决虚拟模板功能设计问题通常并不简单。但在这种特定情况下,最简单的方法可能是复制每个派生类中的operator<<
。但是,在调用stream::operator<<
的情况下,它不起作用,例如:
void func(stream& s) {
s << liquid_crystal::move{ 1, 2 };
}
int main() {
liquid_crystal lcd;
func(lcd);
}
最佳解决方案是每个类自行定义operator<<
。 但是,如果无法做到这一点,则可以滥用SFINAE和动态铸造来达到预期的效果。
我不建议在您的最终产品中使用它。 我只是把它贴在这里,希望有人比我更有知识,可以找到一种方法,在没有dynamic_cast
的情况下完成这项工作,而不会引入可能导致未定义行为的代码。
#include <iostream>
#include <type_traits>
// -----
// void_t definition. If your compiler has provisional C++17 support, this may already be
// available.
template<typename...>
struct make_void { using type = void; };
template<typename... T>
using void_t = typename make_void<T...>::type;
// -----
// SFINAE condition: Is argument a stream command?
class stream;
template<typename T, typename = void>
struct is_stream_command : std::false_type
{};
template<typename T>
struct is_stream_command<T, void_t<typename T::command_for>> :
std::integral_constant<bool, std::is_base_of<stream, typename T::command_for>::value ||
std::is_same<stream, typename T::command_for>::value>
{};
// -----
class stream
{
public:
// Stream command type. Used as parent class for actual commands.
// Necessary for version of print() that handles stream commands.
// Not needed for SFINAE.
// If used, each command will need a constructor.
struct command
{
using command_for = stream;
// This typedef should be defined in each command, as the containing class.
// If command is valid for multiple classes, this should be their parent class.
// Used for SFINAE.
virtual ~command() = default;
};
struct fill : command
{
int n;
char ch;
using command_for = stream;
fill(int n, char ch) : n(n), ch(ch) {}
};
stream()
{}
virtual ~stream()
{}
// Called for stream commands. Solves the issue you were having, but introduces a
// different issue: It casts "this", which can cause problems.
// Tied with a version of stream::print() that handles commands.
template<typename T>
typename std::enable_if<is_stream_command<T>::value, stream&>::type
operator<<(T t)
{
static_cast<typename T::command_for*>(this)->print((stream::command*) &t);
return *this;
}
// Called for other output.
template<typename T>
typename std::enable_if<!is_stream_command<T>::value, stream&>::type
operator<<(T t)
{
this->print(t);
return *this;
}
protected:
virtual void print(char) = 0;
virtual void print(stream::command* com);
template <typename T>
void print(T);
};
template <>
void stream::print<const char*>(const char* str)
{
while (*str != ' ')
this->print(*(str++));
std::cout << std::endl; // For testing.
}
template <>
void stream::print<stream::fill>(stream::fill f)
{
std::cout << "fill "; // For testing.
while (f.n > 0)
{
this->print(f.ch);
f.n--;
}
std::cout << std::endl; // For testing.
}
// Version of print() which handles stream commands.
// Solves problem introduced by operator<<() for commands, but introduces its own problem:
// dynamic casting.
void stream::print(stream::command* com) {
if (dynamic_cast<stream::fill*>(com)) {
std::cout << "Valid command: "; // For testing.
this->print(*(dynamic_cast<stream::fill*>(com)));
} else {
// Handle as appropriate.
std::cout << "Invalid command." << std::endl;
}
}
// -----
class serial_port : public stream
{
public:
struct set_baudrate : stream::command
{
int baud;
using command_for = serial_port;
set_baudrate(int baud) : baud(baud) {}
};
using stream::stream;
private:
void print(char c) override
{
// TODO: print to the actual serial port
std::cout << c;
}
void print(stream::command* com) override;
template <typename T>
void print(T t)
{
stream::print<T>(t);
}
// Necessary to allow stream::operator<<() to call private member function print().
template<typename T>
friend typename std::enable_if<is_stream_command<T>::value, stream&>::type
stream::operator<<(T t);
};
template <>
void serial_port::print<serial_port::set_baudrate>(serial_port::set_baudrate)
{
this->print("set_baudrate");
}
void serial_port::print(stream::command* com) {
if (dynamic_cast<serial_port::set_baudrate*>(com)) {
std::cout << "Valid command: "; // For testing.
this->print(*(dynamic_cast<serial_port::set_baudrate*>(com)));
} else {
// Invalid commands fall through to parent class, in case they're valid for any
// stream.
this->stream::print(com);
}
}
// -----
class liquid_crystal : public stream
{
public:
struct move : stream::command
{
int x;
int y;
using command_for = liquid_crystal;
move(int x, int y) : x(x), y(y) {}
};
using stream::stream;
private:
void print(char c) override
{
// TODO: print to a character LCD
std::cout << c;
}
void print(stream::command* com) override;
template <typename T>
void print(T t)
{
stream::print<T>(t);
}
// Necessary to allow stream::operator<<() to call private member function print().
template<typename T>
friend typename std::enable_if<is_stream_command<T>::value, stream&>::type
stream::operator<<(T t);
};
template <>
void liquid_crystal::print<liquid_crystal::move>(liquid_crystal::move)
{
this->print("move");
}
void liquid_crystal::print(stream::command* com) {
if (dynamic_cast<liquid_crystal::move*>(com)) {
std::cout << "Valid command: "; // For testing.
this->print(*(dynamic_cast<liquid_crystal::move*>(com)));
} else {
// Invalid commands fall through to parent class, in case they're valid for any
// stream.
this->stream::print(com);
}
}
// -----
int main()
{
liquid_crystal lcd;
lcd << 'a' << " " << liquid_crystal::move{ 1, 2 };
serial_port serial;
serial << 'a' << " " << serial_port::set_baudrate{ 9600 };
std::cout << "Are they valid commands?" << std::endl;
std::cout << std::boolalpha;
std::cout << "stream::fill, for serial_port: ";
serial << stream::fill{ 3, 'a' };
std::cout << "stream::fill, for liquid_crystal: ";
lcd << stream::fill{ 3, 'a' };
std::cout << "serial_port::set_baudrate, for serial_port: ";
serial << serial_port::set_baudrate{ 9600 };
std::cout << "serial_port::set_baudrate, for liquid_crystal: ";
lcd << serial_port::set_baudrate{ 9600 };
std::cout << "liquid_crystal::move, for serial_port: ";
serial << liquid_crystal::move{ 1, 2 };
std::cout << "liquid_crystal::move, for liquid_crystal: ";
lcd << liquid_crystal::move{ 1, 2 };
}
这将具有以下输出:
a b
Valid command: move
a b
Valid command: set_baudrate
Are they valid commands?
stream::fill, for serial_port: Valid command: fill aaa
stream::fill, for liquid_crystal: Valid command: fill aaa
serial_port::set_baudrate, for serial_port: Valid command: set_baudrate
serial_port::set_baudrate, for liquid_crystal: Invalid command.
liquid_crystal::move, for serial_port: Invalid command.
liquid_crystal::move, for liquid_crystal: Valid command: move
我不喜欢这个解决方案,因为它依赖于dynamic_cast
. 但是,我发现错误的解决方案通常比没有解决方案要好,因为它可以帮助您找到一个好的解决方案。 这解决了链接问题,理想情况下可以用作不需要任何强制转换的解决方案的基础。
- 对RValue对象调用的LValue ref限定成员函数
- 为什么使用 "this" 指针调用派生成员函数?
- 将公共但非静态的成员函数与ALGLIB集成
- 使用指向成员的指针将成员函数作为参数传递
- 将重载的成员函数传递给函数模板
- 我不小心调用了一个没有自己类对象的成员函数.但这是怎么回事呢
- 如何在C++中使用非静态成员函数作为回调函数
- C++错误C2600:无法定义编译器生成的特殊成员函数(必须首先在类中声明)
- 关联容器的下界复杂性:成员函数与非成员函数
- 在 C++ 中用派生类型重写成员函数
- 链表的泛型函数remove()与成员函数remove)
- 如何将lambda作为模板类的成员函数参数
- constexpr构造函数需要常量成员函数时出现问题
- 将自由函数绑定为类成员函数
- 区分非成员函数和头文件中的成员函数
- 如何从子成员函数修改父公共成员变量
- 保留对其他类的成员函数的引用
- 在运算符重载定义中使用成员函数(const错误)
- 内联如何影响模块接口中的成员函数
- 将成员函数指针作为参数传递给模板方法