了解 C++ 特性并使它们高效
Understanding C++ Traits and Making Them Efficient
我最近遇到了“特征”这个有趣而强大的概念,并试图 understand/implement 在 C++ 中使用它们。据我所知,traits 提供了一种既 extend/adapt 退出代码的功能又为 class 定义“接口”而不使用传统继承(以及它附带的所有 overhead/problems ).我也看到这个概念好像和C++中的CRTP设计模式有密切关系
举个例子,我用 C++ 编写接口的正常思维过程是用纯虚方法定义一个 class。然后我可以创建一个 subclass 并传递一个指向我所有通用代码的指针。但是我发现这有一些问题:
- 类需要从多个接口继承需要使用多重继承,这会变得非常复杂并引入“菱形模式”问题。
- 形成严格的“是”关系,这并不总是本意。例如,如果我正在描述一个灯的接口,模拟灯并不是真正的灯,它只是具有“特性”/像灯一样的行为。我的意思是,通用 Light 接口实际上并没有实现需要继承的共性,它只是定义了实现的行为方式。
- 虚拟方法和继承允许完全动态多态性,这会产生不必要的开销。在我的大部分代码中,我一次只会使用一个接口的实现,因此我不需要动态选择正确的实现,我只需要让接口的“用户”足够通用对于所有不同的实现。
这里是一个简单的、传统的 Light 界面示例:
class Light {
public:
virtual void on() = 0;
virtual void off() = 0;
};
class MyLight : public Light {
public:
void on() override;
void off() override;
};
void lightController(Light& l) {
l.on();
l.off();
}
并且(基于此处的文章:https://chrisbranch.co.uk/2015/02/make-your-c-interfaces-trait-forward/)我认为这是同一概念的“基于特征”的实现:
template<typename T>
class Light {
public:
Light(T& self) : _self(self) {}
void on() { _self.on(); }
void off() { _self.off(); }
private:
T& _self;
};
class MyLight {
public:
void on();
void off();
};
class OddLight {
public:
void set(bool state);
};
template<>
class Light<OddLight> {
public:
Light(OddLight& self) : _self(self) {}
void on() { _self.set(true); }
void off() { _self.set(false); }
private:
OddLight& _self;
};
template<typename T>
void lightUser1(T& l) {
Light<T> light(l);
light.on();
light.off();
}
template<typename T>
void lightUser2(Light<T>& l) {
light.on();
light.off();
}
我对此有几个问题:
- 因为,要使用这样的特性,您(临时)创建了一个新的 Light 实例,是否有与此相关的内存开销?
- 是否有更有效的方法来记录特定的class“实现”给定特征?
- 文章提到了两种为接口定义“用户”的方法。我已经在上面展示了两者。 lightUser2 似乎是最完善的文档(它明确指出该函数需要 Light 特性的某些实现),但是它要求将实现显式转换为函数外部的 Light。有没有方法既可以记录用户的意图,又可以直接传递所有实现?
谢谢!
这看起来像一个适配器,而不是 C++ 中使用的特性。
C++ 中的 Traits 类似于 std::numeric_limits
或 std::iterator_traits
。它需要一个类型和 returns 一些关于该类型的信息。默认实现处理一定数量的情况,您可以专门处理其他情况。
他写的代码有几个问题。
在 Rust 中,这用于动态调度。模板版本不是动态的。
C++ 在值类型上蓬勃发展。对于嵌入式引用,这不能是值类型。
检查晚了,在鸭子打字时,错误出现在特征代码中,而不是在调用站点。
另一种方法是使用免费功能和概念以及 ADL。
turn_light_on(foo)
和 turn_light_off(foo)
可以默认并通过 ADL 找到,允许自定义现有类型。如果您想避免“一个命名空间”问题,您可以包含一个接口标记。
namespace Light {
struct light_tag{};
template<class T>
concept LightClass = requires(T& a) {
{ a.on() };
{ a.off() };
};
void on(light_tag, LightClass auto& light){ light.on(); }
void off(light_tag, LightClass auto& light){ light.off(); }
// also, a `bool` is a light, right?
void on(light_tag, bool& light){ light=true; }
void off(light_tag, bool& light){ light=false; }
template<class T>
concept Light = requires(T& a) {
{ on( light_tag{}, a ) };
{ off( light_tag{}, a ) };
};
void lightController(Light auto& l) {
on(light_tag{}, l);
off(light_tag{}, l);
}
struct SimpleLight {
bool bright = false;
void on() { bright = true; }
void off() { bright = false; }
};
}
然后我们有 OddLight
:
namespace Odd {
class OddLight {
public:
void set(bool state);
};
}
我们希望它是 Light
,所以我们这样做:
namespace Odd {
void on(::Light::light_tag, OddLight& odd){ odd.set(true); }
void off(::Light::light_tag, OddLight& odd){ odd.set(false); }
}
然后
struct not_a_light{};
如果我们有测试代码:
int main() {
Light::SimpleLight simple;
Odd::OddLight odd;
not_a_light notLight;
Light::lightController(simple);
Light::lightController(odd);
// Light::lightController(notLight); // fails to compile, error is here
}
注意概念图:
namespace Odd {
void on(::Light::light_tag, OddLight& odd){ odd.set(true); }
void off(::Light::light_tag, OddLight& odd){ odd.set(false); }
}
可以在namespace Odd
或namespace Light
中定义。
如果你想将其扩展到动态调度,你必须手动编写类型擦除。
namespace Light {
struct PolyLightVtable {
void (*on)(void*) = nullptr;
void (*off)(void*) = nullptr;
template<Light T>
static constexpr PolyLightVtable make() {
using Light::on;
using Light::off;
return {
[](void* p){ on( light_tag{}, *static_cast<T*>(p) ); },
[](void* p){ off( light_tag{}, *static_cast<T*>(p) ); }
};
}
template<Light T>
static PolyLightVtable const* get() {
static constexpr auto retval = make<T>();
return &retval;
}
};
struct PolyLightRef {
PolyLightVtable const* vtable = 0;
void* state = 0;
void on() {
vtable->on(state);
}
void off() {
vtable->off(state);
}
template<Light T> requires (!std::is_same_v<std::decay_t<T>, PolyLightRef>)
PolyLightRef( T& l ):
vtable( PolyLightVtable::get<std::decay_t<T>>() ),
state(std::addressof(l))
{}
};
}
现在我们可以写:
void foo( Light::PolyLightRef light ) {
light.on();
light.off();
}
我们得到了动态调度; foo
的定义可以对调用者隐藏。
将 PolyLightRef
扩展到 PolyLightValue
并不那么棘手——我们只需将 assign(move/copy)/construct(move/copy)/destroy 添加到 vtable ,然后将状态填充到 void*
中的堆中,或者在某些情况下使用小缓冲区优化。
现在我们有了一个完整的基于动态“特征”的 rust 式系统,特征在入口点进行测试(当您将它们作为 Light auto
或 PolyLightYYY
传递时), 特征命名空间 或类型命名空间中的 中的自定义等
就个人而言,我期待 c++23 进行反思,并期待将上述一些样板文件自动化的可能性。
实际上有一个有用变体的网格:
RuntimePoly CompiletimePoly Concepts
PolyLightRef LightRef<T> Light&
PolyLightValue LightValue<T> Light
你可以用类似 Rust 的方式来解决这个问题。
c++17 推导指南可用于使 CompiletimePoly
使用起来不那么烦人:
LightRef ref = light;
可以用
为你演绎T
template<class T>
LightRef(T&)->LightRef<T>;
(这可能是为你写的),并在调用现场
LightRefTemplateTakingFunction( LightRef{foo} )
我最近遇到了“特征”这个有趣而强大的概念,并试图 understand/implement 在 C++ 中使用它们。据我所知,traits 提供了一种既 extend/adapt 退出代码的功能又为 class 定义“接口”而不使用传统继承(以及它附带的所有 overhead/problems ).我也看到这个概念好像和C++中的CRTP设计模式有密切关系
举个例子,我用 C++ 编写接口的正常思维过程是用纯虚方法定义一个 class。然后我可以创建一个 subclass 并传递一个指向我所有通用代码的指针。但是我发现这有一些问题:
- 类需要从多个接口继承需要使用多重继承,这会变得非常复杂并引入“菱形模式”问题。
- 形成严格的“是”关系,这并不总是本意。例如,如果我正在描述一个灯的接口,模拟灯并不是真正的灯,它只是具有“特性”/像灯一样的行为。我的意思是,通用 Light 接口实际上并没有实现需要继承的共性,它只是定义了实现的行为方式。
- 虚拟方法和继承允许完全动态多态性,这会产生不必要的开销。在我的大部分代码中,我一次只会使用一个接口的实现,因此我不需要动态选择正确的实现,我只需要让接口的“用户”足够通用对于所有不同的实现。
这里是一个简单的、传统的 Light 界面示例:
class Light {
public:
virtual void on() = 0;
virtual void off() = 0;
};
class MyLight : public Light {
public:
void on() override;
void off() override;
};
void lightController(Light& l) {
l.on();
l.off();
}
并且(基于此处的文章:https://chrisbranch.co.uk/2015/02/make-your-c-interfaces-trait-forward/)我认为这是同一概念的“基于特征”的实现:
template<typename T>
class Light {
public:
Light(T& self) : _self(self) {}
void on() { _self.on(); }
void off() { _self.off(); }
private:
T& _self;
};
class MyLight {
public:
void on();
void off();
};
class OddLight {
public:
void set(bool state);
};
template<>
class Light<OddLight> {
public:
Light(OddLight& self) : _self(self) {}
void on() { _self.set(true); }
void off() { _self.set(false); }
private:
OddLight& _self;
};
template<typename T>
void lightUser1(T& l) {
Light<T> light(l);
light.on();
light.off();
}
template<typename T>
void lightUser2(Light<T>& l) {
light.on();
light.off();
}
我对此有几个问题:
- 因为,要使用这样的特性,您(临时)创建了一个新的 Light 实例,是否有与此相关的内存开销?
- 是否有更有效的方法来记录特定的class“实现”给定特征?
- 文章提到了两种为接口定义“用户”的方法。我已经在上面展示了两者。 lightUser2 似乎是最完善的文档(它明确指出该函数需要 Light 特性的某些实现),但是它要求将实现显式转换为函数外部的 Light。有没有方法既可以记录用户的意图,又可以直接传递所有实现?
谢谢!
这看起来像一个适配器,而不是 C++ 中使用的特性。
C++ 中的 Traits 类似于 std::numeric_limits
或 std::iterator_traits
。它需要一个类型和 returns 一些关于该类型的信息。默认实现处理一定数量的情况,您可以专门处理其他情况。
他写的代码有几个问题。
在 Rust 中,这用于动态调度。模板版本不是动态的。
C++ 在值类型上蓬勃发展。对于嵌入式引用,这不能是值类型。
检查晚了,在鸭子打字时,错误出现在特征代码中,而不是在调用站点。
另一种方法是使用免费功能和概念以及 ADL。
turn_light_on(foo)
和 turn_light_off(foo)
可以默认并通过 ADL 找到,允许自定义现有类型。如果您想避免“一个命名空间”问题,您可以包含一个接口标记。
namespace Light {
struct light_tag{};
template<class T>
concept LightClass = requires(T& a) {
{ a.on() };
{ a.off() };
};
void on(light_tag, LightClass auto& light){ light.on(); }
void off(light_tag, LightClass auto& light){ light.off(); }
// also, a `bool` is a light, right?
void on(light_tag, bool& light){ light=true; }
void off(light_tag, bool& light){ light=false; }
template<class T>
concept Light = requires(T& a) {
{ on( light_tag{}, a ) };
{ off( light_tag{}, a ) };
};
void lightController(Light auto& l) {
on(light_tag{}, l);
off(light_tag{}, l);
}
struct SimpleLight {
bool bright = false;
void on() { bright = true; }
void off() { bright = false; }
};
}
然后我们有 OddLight
:
namespace Odd {
class OddLight {
public:
void set(bool state);
};
}
我们希望它是 Light
,所以我们这样做:
namespace Odd {
void on(::Light::light_tag, OddLight& odd){ odd.set(true); }
void off(::Light::light_tag, OddLight& odd){ odd.set(false); }
}
然后
struct not_a_light{};
如果我们有测试代码:
int main() {
Light::SimpleLight simple;
Odd::OddLight odd;
not_a_light notLight;
Light::lightController(simple);
Light::lightController(odd);
// Light::lightController(notLight); // fails to compile, error is here
}
注意概念图:
namespace Odd {
void on(::Light::light_tag, OddLight& odd){ odd.set(true); }
void off(::Light::light_tag, OddLight& odd){ odd.set(false); }
}
可以在namespace Odd
或namespace Light
中定义。
如果你想将其扩展到动态调度,你必须手动编写类型擦除。
namespace Light {
struct PolyLightVtable {
void (*on)(void*) = nullptr;
void (*off)(void*) = nullptr;
template<Light T>
static constexpr PolyLightVtable make() {
using Light::on;
using Light::off;
return {
[](void* p){ on( light_tag{}, *static_cast<T*>(p) ); },
[](void* p){ off( light_tag{}, *static_cast<T*>(p) ); }
};
}
template<Light T>
static PolyLightVtable const* get() {
static constexpr auto retval = make<T>();
return &retval;
}
};
struct PolyLightRef {
PolyLightVtable const* vtable = 0;
void* state = 0;
void on() {
vtable->on(state);
}
void off() {
vtable->off(state);
}
template<Light T> requires (!std::is_same_v<std::decay_t<T>, PolyLightRef>)
PolyLightRef( T& l ):
vtable( PolyLightVtable::get<std::decay_t<T>>() ),
state(std::addressof(l))
{}
};
}
现在我们可以写:
void foo( Light::PolyLightRef light ) {
light.on();
light.off();
}
我们得到了动态调度; foo
的定义可以对调用者隐藏。
将 PolyLightRef
扩展到 PolyLightValue
并不那么棘手——我们只需将 assign(move/copy)/construct(move/copy)/destroy 添加到 vtable ,然后将状态填充到 void*
中的堆中,或者在某些情况下使用小缓冲区优化。
现在我们有了一个完整的基于动态“特征”的 rust 式系统,特征在入口点进行测试(当您将它们作为 Light auto
或 PolyLightYYY
传递时), 特征命名空间 或类型命名空间中的 中的自定义等
就个人而言,我期待 c++23 进行反思,并期待将上述一些样板文件自动化的可能性。
实际上有一个有用变体的网格:
RuntimePoly CompiletimePoly Concepts
PolyLightRef LightRef<T> Light&
PolyLightValue LightValue<T> Light
你可以用类似 Rust 的方式来解决这个问题。
c++17 推导指南可用于使 CompiletimePoly
使用起来不那么烦人:
LightRef ref = light;
可以用
为你演绎T
template<class T>
LightRef(T&)->LightRef<T>;
(这可能是为你写的),并在调用现场
LightRefTemplateTakingFunction( LightRef{foo} )