了解 C++ 特性并使它们高效

Understanding C++ Traits and Making Them Efficient

我最近遇到了“特征”这个有趣而强大的概念,并试图 understand/implement 在 C++ 中使用它们。据我所知,traits 提供了一种既 extend/adapt 退出代码的功能又为 class 定义“接口”而不使用传统继承(以及它附带的所有 overhead/problems ).我也看到这个概念好像和C++中的CRTP设计模式有密切关系

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

  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();
}

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

我对此有几个问题:

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

谢谢!

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

C++ 中的 Traits 类似于 std::numeric_limitsstd::iterator_traits。它需要一个类型和 returns 一些关于该类型的信息。默认实现处理一定数量的情况,您可以专门处理其他情况。


他写的代码有几个问题。

  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; }
  };
}

然后我们有 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 Oddnamespace 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 autoPolyLightYYY 传递时), 特征命名空间 或类型命名空间中的 中的自定义等

就个人而言,我期待 进行反思,并期待将上述一些样板文件自动化的可能性。


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

RuntimePoly        CompiletimePoly     Concepts
PolyLightRef       LightRef<T>         Light&
PolyLightValue     LightValue<T>       Light

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

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

LightRef ref = light;

可以用

为你演绎T
template<class T>
LightRef(T&)->LightRef<T>;

(这可能是为你写的),并在调用现场

LightRefTemplateTakingFunction( LightRef{foo} )

Live example with error messages