了解 C++ 特性并使其高效

Pat*_*ght 10 c++ interface traits

我最近遇到了“特质”这个有趣且强大的概念,并尝试在 C++ 中理解/实现它们。据我了解,特征提供了一种方法,既可以扩展/调整现有代码的功能,又可以为类定义“接口”,而无需使用传统继承(以及随之而来的所有开销/问题)。我还看到这个概念似乎与C++中的CRTP设计模式密切相关。

举个例子,我用 C++ 编写接口的正常思维过程是定义一个具有纯虚方法的类。然后我可以创建它的子类并将指针传递给我的所有通用代码。然而我发现这有一些问题:

  1. 需要从多个接口继承的类需要使用多重继承,这可能会变得非常复杂并引入“钻石模式”问题。
  2. 形成严格的“是”关系,但这并不总是意图。例如,如果我描述光的接口,则模拟光并不是真正的光,它仅具有“特征”/像光一样起作用。我的意思是,通用 Light 接口并不真正具有实现需要继承的共性,它只是定义了实现应该如何表现。
  3. 虚拟方法和继承允许完全动态多态性,这会产生不必要的开销。在我的大多数代码中,我一次只会使用接口的单个​​实现,因此我不需要动态选择正确的实现,我只需要让接口的“用户”足够通用对于不同的实现。

下面是一个简单、传统的 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();
}
Run Code Online (Sandbox Code Playgroud)

并且(基于此处的文章: 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();
}
Run Code Online (Sandbox Code Playgroud)

我对此有几个问题:

  1. 因为,要使用这样的特征,您(临时)创建一个新的 Light 实例,是否有与此相关的内存开销?
  2. 是否有更有效的方法来记录特定类“实现”给定特征?
  3. 本文提到了两种为界面定义“用户”的方法。我已经在上面展示了两者。lightUser2 似乎是最完善的文档(它明确指出该函数需要 Light 特征的某些实现),但是它要求将实现显式转换为函数外部的 Light。是否有方法可以同时记录用户的意图和直接传递的所有实现?

谢谢你!

Yak*_*ont 6

这看起来像一个适配器,而不是 C++ 中使用的特征。

C++ 中的 Traits 就像std::numeric_limitsor std::iterator_traits。它接受一个类型并返回有关该类型的一些信息。默认实现处理一定数量的情况,您可以将其专门化来处理其他情况。


他编写的代码有一些问题。

  1. 在 Rust 中,这用于动态调度。模板版本不是动态的。

  2. C++ 在值类型上蓬勃发展。对于嵌入引用,这不能是值类型。

  3. 检查很晚,在鸭子打字时,错误显示在特征代码中,而不是在调用站点上。

另一种方法是使用自由函数和概念以及 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; }
  };
}
Run Code Online (Sandbox Code Playgroud)

然后我们就有了OddLight

namespace Odd {
  class OddLight {
  public:
    void set(bool state);
  };
}
Run Code Online (Sandbox Code Playgroud)

我们希望它是 a Light,所以我们这样做:

namespace Odd {
  void on(::Light::light_tag, OddLight& odd){ odd.set(true); }
  void off(::Light::light_tag, OddLight& odd){ odd.set(false); }
}
Run Code Online (Sandbox Code Playgroud)

然后

struct not_a_light{};
Run Code Online (Sandbox Code Playgroud)

如果我们有测试代码:

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
}
Run Code Online (Sandbox Code Playgroud)

请注意概念图:

namespace Odd {
  void on(::Light::light_tag, OddLight& odd){ odd.set(true); }
  void off(::Light::light_tag, OddLight& odd){ odd.set(false); }
}
Run Code Online (Sandbox Code Playgroud)

可以在 或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))
    {}
  };
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以写:

void foo( Light::PolyLightRef light ) {
    light.on();
    light.off();
}
Run Code Online (Sandbox Code Playgroud)

我们得到动态调度;的定义foo可以对调用者隐藏。

扩展PolyLightRefPolyLightValue并不是那么棘手——我们只需将分配(移动/复制)/构造(移动/复制)/销毁添加到vtable中,然后将状态填充到堆中或在void*某些情况下使用小缓冲区优化。

现在我们有一个完整的 Rust 式系统,基于动态“特征”的调度,特征在入口点进行测试(当您将它们作为Light auto或传递时PolyLightYYY),在特征命名空间类型的命名空间中进行自定义,等等。

我个人期待具有反射功能,以及自动化上述一些样板的可能性。


实际上有一个有用的变体网格:

RuntimePoly        CompiletimePoly     Concepts
PolyLightRef       LightRef<T>         Light&
PolyLightValue     LightValue<T>       Light
Run Code Online (Sandbox Code Playgroud)

你可以用类似 Rust 的方式来解决这个问题。

推导指南可用于使CompiletimePoly使用起来不那么烦人:

LightRef ref = light;
Run Code Online (Sandbox Code Playgroud)

可以T为你推断

template<class T>
LightRef(T&)->LightRef<T>;
Run Code Online (Sandbox Code Playgroud)

(这可能是为您写的),并在呼叫站点

LightRefTemplateTakingFunction( LightRef{foo} )
Run Code Online (Sandbox Code Playgroud)

带有错误消息的实时示例