实现模板专用函数
Implementing template specialized functions
我正在尝试允许 class 实现一个接口,该接口被模板化为允许或不允许内部更新。如果实现在 class 声明中,代码看起来像这样并且可以工作。如果它被移出(如此处所示),我会在 Visual Studio 2019 收到编译器错误,我不明白为什么。感谢所有帮助!
以下代码将无法编译。如果将 class 实现和 headers 修改为仅使用我注释掉的代码,它会按预期编译和工作。
#include <atomic>
#include <cstdint>
#include <memory>
#include <iostream>
#include <string>
enum class EntryType { Read, ReadUpdate };
template <EntryType Type>
class IXQ
{
public:
virtual float GetValue() = 0;
};
class X : public IXQ<EntryType::Read>, public IXQ<EntryType::ReadUpdate>
{
public:
float IXQ<EntryType::Read>::GetValue();
/*
float IXQ<EntryType::Read>::GetValue()
{
return _x;
}
*/
float IXQ<EntryType::ReadUpdate>::GetValue();
/*
float IXQ<EntryType::ReadUpdate>::GetValue() {
_counter++;
return _x;
}
*/
float _x = 10.0F;
std::atomic<std::int32_t> _counter = 0;
};
float X::IXQ<EntryType::Read>::GetValue()
{
return _x;
}
float X::IXQ<EntryType::ReadUpdate>::GetValue()
{
_counter++;
return _x;
};
int main(int argc, char* argv[])
{
{
auto k = std::make_unique<X>();
auto ptrQ = static_cast<IXQ<EntryType::Read>*>(k.get());
std::cout << std::to_string(ptrQ->GetValue()) << std::endl;
std::cout << k->_counter.load() << std::endl;
}
{
auto k = std::make_unique<X>();
auto ptrQ = static_cast<IXQ<EntryType::ReadUpdate>*>(k.get());
std::cout << std::to_string(ptrQ->GetValue()) << std::endl;
std::cout << k->_counter.load() << std::endl;
}
return 0;
}
我认为这可以通过添加类的中间层来解决,假设X_Read
和X_ReadUpdate
可以继承X
。
IXQ
/ \
X_Read X_ReadUpdate
\ /
X
诀窍是让那些中间 类 通过调用两个虚拟纯方法实现 GetValue
,假设分别是 GetValueRead
和 GetValueReadUpdate
。这样,X
仍然继承了IXQ<EntryType::Read>
和IXQ<EntryType::ReadUpdate>
,但是实现了GetValueRead
和GetValueReadUpdate
,这两个方法接口清晰明了。
template <EntryType Type>
class IXQ
{
public:
virtual float GetValue() = 0;
};
class X_Read : public IXQ<EntryType::Read>
{
public:
virtual float GetValue() override { return GetValueRead(); };
virtual float GetValueRead() = 0;
};
class X_ReadUpdate : public IXQ<EntryType::ReadUpdate>
{
public:
virtual float GetValue() override { return GetValueReadUpdate(); };
virtual float GetValueReadUpdate() = 0;
};
class X : public X_Read, public X_ReadUpdate
{
public:
virtual float GetValueRead() override { return _x; }
virtual float GetValueReadUpdate() { _counter++; return _x; };
float _x{10.0f};
std::atomic<std::int32_t> _counter{};
};
// Outputs:
// 10.000000
// 0
// 10.000000
// 1
1。 Microsoft 特定语法
您用来覆盖 GetValue()
的语法是 不是 标准 c++。
class X : public IXQ<EntryType::Read> {
public:
float IXQ<EntryType::Read>::GetValue() { /* ... */ }
}
这是 Microsoft 特定的 C++ 扩展,称为 Explicit Overrides (C++) and is intended to be used with __interface
s(也是 Microsoft 特定的 c++ 扩展)。
__interface
s 不正式支持 c++ 模板,因为它们主要用于 COM 接口,例如:
[ object, uuid("00000000-0000-0000-0000-000000000001"), library_block ]
__interface ISomething {
// properties
[ id(0) ] int iInt;
[ id(5) ] BSTR bStr;
// functions
void DoSomething();
};
令人惊讶的是 float IXQ<EntryType::Read>::GetValue() { /* ... */ }
完全有效。
2。符合标准 C++ 的解决方案
在标准 C++ 中,被覆盖的函数仅由名称和参数决定,因此您唯一的选择是同时覆盖它们。
例如:
class X : public IXQ<EntryType::Read>, public IXQ<EntryType::ReadUpdate>
{
public:
// will be the implementation for both IXQ<EntryType::Read> & IXQ<EntryType::ReadUpdate>
float GetValue() override;
};
float X::GetValue() {
/* ... */
}
另一种符合标准的方法是创建 2 个 class 继承给定接口的类,然后让 X 从它们两个继承:
class XRead : public IXQ<EntryType::Read> {
public:
float GetValue() override { /* ... */ }
};
class XReadUpdate : public IXQ<EntryType::ReadUpdate> {
public:
float GetValue() override { /* ... */ }
};
class X : public XRead, public XReadUpdate {
/* ... */
};
如果你想在 XRead
和 XReadUpdate
之间共享状态,你必须引入另一个级别,例如:
class XBase {
public:
virtual ~XBase() = default;
protected:
float value;
};
class XRead : public virtual XBase, public IXQ<EntryType::Read> {
float GetValue() override { return value; }
};
class XReadUpdate : public virtual XBase, public IXQ<EntryType::ReadUpdate> {
float GetValue() override { return value; }
};
class X : public XRead, public XReadUpdate {
/* ... */
};
3。 API 设计
的可能建议
请记住,这种 API 设计使用起来会相当复杂,因为您总是需要先转换到特定接口,然后才能调用该函数,因为 X{}.GetValue()
会模棱两可。
例如:
X x;
IXQ<EntryType::Read>& read = x;
std::cout << read.GetValue() << std::endl;
IXQ<EntryType::ReadUpdate>& update = x;
std::cout << update.GetValue() << std::endl;
// this is *not* possible, GetValue() is ambigous
// std::cout << x.GetValue() << std::endl;
我建议将两个接口分开并使用不同的方法名称,例如:godbolt example
struct IXQReadonly {
virtual ~IXQReadonly() = default;
virtual float GetValue() = 0;
};
struct IXQ : IXQReadonly {
virtual float GetValueUpdate() = 0;
};
class X : public IXQ {
public:
X() : value(0.0f), counter(0) { }
float GetValue() override { return value; }
float GetValueUpdate() override {
++counter;
return value;
}
private:
float value;
std::atomic<int> counter;
};
这带来了一整套好处:
- 可以直接在
X
上调用GetValue()
/GetValueUpdate()
,不需要先转成接口
X x;
x.GetValue();
x.GetValueUpdate();
- 如果它会更新计数器,从方法名称中可以立即清楚
- 给定
IXQ
的函数可以调用期望 IXQReadonly
的函数,但反过来不行:(因此您可以将读写接口“降级”为只读,但不是将只读接口“升级”为读写)
void bar(IXQReadonly& r) { r.GetValue(); /* we can't do GetValueUpdate() here */ }
void foo(IXQ& x) { bar(x); x.GetValueUpdate(); }
另外记得将析构函数声明为 virtual
(至少在最顶层的 class / 接口中),否则如果您尝试删除 [= 的实例,往往会发生有趣的事情28=] 通过指向其基/接口之一的指针。
我正在尝试允许 class 实现一个接口,该接口被模板化为允许或不允许内部更新。如果实现在 class 声明中,代码看起来像这样并且可以工作。如果它被移出(如此处所示),我会在 Visual Studio 2019 收到编译器错误,我不明白为什么。感谢所有帮助!
以下代码将无法编译。如果将 class 实现和 headers 修改为仅使用我注释掉的代码,它会按预期编译和工作。
#include <atomic>
#include <cstdint>
#include <memory>
#include <iostream>
#include <string>
enum class EntryType { Read, ReadUpdate };
template <EntryType Type>
class IXQ
{
public:
virtual float GetValue() = 0;
};
class X : public IXQ<EntryType::Read>, public IXQ<EntryType::ReadUpdate>
{
public:
float IXQ<EntryType::Read>::GetValue();
/*
float IXQ<EntryType::Read>::GetValue()
{
return _x;
}
*/
float IXQ<EntryType::ReadUpdate>::GetValue();
/*
float IXQ<EntryType::ReadUpdate>::GetValue() {
_counter++;
return _x;
}
*/
float _x = 10.0F;
std::atomic<std::int32_t> _counter = 0;
};
float X::IXQ<EntryType::Read>::GetValue()
{
return _x;
}
float X::IXQ<EntryType::ReadUpdate>::GetValue()
{
_counter++;
return _x;
};
int main(int argc, char* argv[])
{
{
auto k = std::make_unique<X>();
auto ptrQ = static_cast<IXQ<EntryType::Read>*>(k.get());
std::cout << std::to_string(ptrQ->GetValue()) << std::endl;
std::cout << k->_counter.load() << std::endl;
}
{
auto k = std::make_unique<X>();
auto ptrQ = static_cast<IXQ<EntryType::ReadUpdate>*>(k.get());
std::cout << std::to_string(ptrQ->GetValue()) << std::endl;
std::cout << k->_counter.load() << std::endl;
}
return 0;
}
我认为这可以通过添加类的中间层来解决,假设X_Read
和X_ReadUpdate
可以继承X
。
IXQ
/ \
X_Read X_ReadUpdate
\ /
X
诀窍是让那些中间 类 通过调用两个虚拟纯方法实现 GetValue
,假设分别是 GetValueRead
和 GetValueReadUpdate
。这样,X
仍然继承了IXQ<EntryType::Read>
和IXQ<EntryType::ReadUpdate>
,但是实现了GetValueRead
和GetValueReadUpdate
,这两个方法接口清晰明了。
template <EntryType Type>
class IXQ
{
public:
virtual float GetValue() = 0;
};
class X_Read : public IXQ<EntryType::Read>
{
public:
virtual float GetValue() override { return GetValueRead(); };
virtual float GetValueRead() = 0;
};
class X_ReadUpdate : public IXQ<EntryType::ReadUpdate>
{
public:
virtual float GetValue() override { return GetValueReadUpdate(); };
virtual float GetValueReadUpdate() = 0;
};
class X : public X_Read, public X_ReadUpdate
{
public:
virtual float GetValueRead() override { return _x; }
virtual float GetValueReadUpdate() { _counter++; return _x; };
float _x{10.0f};
std::atomic<std::int32_t> _counter{};
};
// Outputs:
// 10.000000
// 0
// 10.000000
// 1
1。 Microsoft 特定语法
您用来覆盖 GetValue()
的语法是 不是 标准 c++。
class X : public IXQ<EntryType::Read> {
public:
float IXQ<EntryType::Read>::GetValue() { /* ... */ }
}
这是 Microsoft 特定的 C++ 扩展,称为 Explicit Overrides (C++) and is intended to be used with __interface
s(也是 Microsoft 特定的 c++ 扩展)。
__interface
s 不正式支持 c++ 模板,因为它们主要用于 COM 接口,例如:
[ object, uuid("00000000-0000-0000-0000-000000000001"), library_block ]
__interface ISomething {
// properties
[ id(0) ] int iInt;
[ id(5) ] BSTR bStr;
// functions
void DoSomething();
};
令人惊讶的是 float IXQ<EntryType::Read>::GetValue() { /* ... */ }
完全有效。
2。符合标准 C++ 的解决方案
在标准 C++ 中,被覆盖的函数仅由名称和参数决定,因此您唯一的选择是同时覆盖它们。
例如:
class X : public IXQ<EntryType::Read>, public IXQ<EntryType::ReadUpdate>
{
public:
// will be the implementation for both IXQ<EntryType::Read> & IXQ<EntryType::ReadUpdate>
float GetValue() override;
};
float X::GetValue() {
/* ... */
}
另一种符合标准的方法是创建 2 个 class 继承给定接口的类,然后让 X 从它们两个继承:
class XRead : public IXQ<EntryType::Read> {
public:
float GetValue() override { /* ... */ }
};
class XReadUpdate : public IXQ<EntryType::ReadUpdate> {
public:
float GetValue() override { /* ... */ }
};
class X : public XRead, public XReadUpdate {
/* ... */
};
如果你想在 XRead
和 XReadUpdate
之间共享状态,你必须引入另一个级别,例如:
class XBase {
public:
virtual ~XBase() = default;
protected:
float value;
};
class XRead : public virtual XBase, public IXQ<EntryType::Read> {
float GetValue() override { return value; }
};
class XReadUpdate : public virtual XBase, public IXQ<EntryType::ReadUpdate> {
float GetValue() override { return value; }
};
class X : public XRead, public XReadUpdate {
/* ... */
};
3。 API 设计
的可能建议请记住,这种 API 设计使用起来会相当复杂,因为您总是需要先转换到特定接口,然后才能调用该函数,因为 X{}.GetValue()
会模棱两可。
例如:
X x;
IXQ<EntryType::Read>& read = x;
std::cout << read.GetValue() << std::endl;
IXQ<EntryType::ReadUpdate>& update = x;
std::cout << update.GetValue() << std::endl;
// this is *not* possible, GetValue() is ambigous
// std::cout << x.GetValue() << std::endl;
我建议将两个接口分开并使用不同的方法名称,例如:godbolt example
struct IXQReadonly {
virtual ~IXQReadonly() = default;
virtual float GetValue() = 0;
};
struct IXQ : IXQReadonly {
virtual float GetValueUpdate() = 0;
};
class X : public IXQ {
public:
X() : value(0.0f), counter(0) { }
float GetValue() override { return value; }
float GetValueUpdate() override {
++counter;
return value;
}
private:
float value;
std::atomic<int> counter;
};
这带来了一整套好处:
- 可以直接在
X
上调用GetValue()
/GetValueUpdate()
,不需要先转成接口X x; x.GetValue(); x.GetValueUpdate();
- 如果它会更新计数器,从方法名称中可以立即清楚
- 给定
IXQ
的函数可以调用期望IXQReadonly
的函数,但反过来不行:(因此您可以将读写接口“降级”为只读,但不是将只读接口“升级”为读写)void bar(IXQReadonly& r) { r.GetValue(); /* we can't do GetValueUpdate() here */ } void foo(IXQ& x) { bar(x); x.GetValueUpdate(); }
另外记得将析构函数声明为 virtual
(至少在最顶层的 class / 接口中),否则如果您尝试删除 [= 的实例,往往会发生有趣的事情28=] 通过指向其基/接口之一的指针。