了解C ++特性并使其高效 [英] Understanding C++ Traits and Making Them Efficient

查看:58
本文介绍了了解C ++特性并使其高效的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我遇到了有趣且强大的特质"概念.最近,正在尝试用C ++理解/实现它们.据我了解,特征提供了一种既扩展/适应现有代码功能又定义接口"的方式.对于一个不使用传统继承的类(及其附带的所有开销/问题).我还看到这个概念似乎与C ++中的CRTP设计模式密切相关.

作为一个例子,我用C ++编写接口的常规思路是用纯虚方法定义一个类.然后,我可以创建此子类,并将指针传递给所有通用代码.我发现这有一些问题:

    需要从多个接口继承的类需要使用多个继承,这会变得非常复杂并引入钻石模式".问题.
  1. 严格的是"关系并非总是意图形成的.例如,如果我描述的是灯光的接口,则模拟的灯光并不是真正的灯光,它仅具有特征"/行为,就像灯光一样.我的意思是,通用的Light接口实际上并没有实现需要继承的共通性,它仅定义了实现应如何表现.
  2. 虚拟方法和继承允许完全动态的多态性,这会导致不必要的开销.在我的大多数代码中,我一次只能使用一个接口的单个​​实现,因此,我不需要动态选择正确的实现,而只需要让"users"(用户)即可.接口的通用性足以满足不同实现的所有需求.

以下是Light的简单传统界面的示例:

  class Light {上市:虚拟void on()= 0;虚拟void off()= 0;};MyLight类:公共照明{上市:void on()覆盖;void off()覆盖;};void lightController(Light& l){l.on();l.off();} 

并且(根据此处的文章:https://chrisbranch.co.uk/2015/02/make-your-c-interfaces-trait-forward/),这就是我认为的基于特征的"相同概念的实现:

  template< typename T>类别Light {上市:Light(T& self):_ self(self){}无效on(){_self.on();}无效off(){_self.off();}私人的:夯;_自己;};类MyLight {上市:无效on();无效off();};OddLight类{上市:无效集(布尔状态);};模板<>类别Light< OddLight>{上市:Light(OddLight& self):_ self(self){}无效on(){_self.set(true);}void off(){_self.set(false);}私人的:奇光灯_自己;};template< typename T>void lightUser1(T& l){光T轻(l);点亮();light.off();}template< typename T>void lightUser2(Light T& l){点亮();light.off();} 

我对此有一些疑问:

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

谢谢!

解决方案

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

C ++中的特征类似于 std :: numeric_limits std :: iterator_traits .它接受一个类型,并返回有关该类型的一些信息.默认实现可处理一定数量的案件,您可以专门处理其他案件.


他编写的代码有几个问题.

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

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

  3. 检查时间很晚,是在鸭子输入时出现的,错误显示在特征码中,而不是在呼叫站点.

另一种替代方法是使用自由函数和概念以及ADL.

可以默认设置

turn_light_on(foo) turn_light_off(foo),并允许自定义现有类型的ADL.如果您要避免使用一个名称空间",问题,您可以包含接口标签.

 命名空间指示灯{struct light_tag {};模板< T类>概念LightClass = require(T& a){{a.on()};{a.off()};};void on(light_tag,LightClass auto& light){light.on();}无效关闭(light_tag,LightClass自动照明){light.off();}//同样,"bool"是一盏灯,对吗?void on(light_tag,bool& light){light = true;}无效关闭(light_tag,bool& light){light = false;}模板< T类>概念灯=要求(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 {布尔明亮=假;无效on(){bright = true;}void off(){bright = false;}};} 

然后我们有我们的 OddLight :

 命名空间奇数{OddLight类{上市:无效集(布尔状态);};} 

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

 命名空间奇数{无效on(:: Light :: light_tag,OddLight&奇数){奇数集(true);}无效关闭(:: Light :: light_tag,OddLight&奇数){奇数设置(false);}} 

然后

  struct not_a_light {}; 

如果我们有测试代码:

  int main(){Light :: SimpleLight简单;Odd :: OddLight奇数;not_a_light notLight;Light :: lightController(简单);Light :: lightController(odd);//Light :: lightController(notLight);//编译失败,错误在这里} 

请注意概念图:

 命名空间奇数{无效on(:: Light :: light_tag,OddLight&奇数){奇数集(true);}无效关闭(:: Light :: light_tag,OddLight&奇数){奇数设置(false);}} 

可以在 namespace Odd namespace Light 中定义

.

如果要将其扩展到动态调度,则必须手动编写类型擦除.

 命名空间指示灯{struct PolyLightVtable {void(* on)(void *)= nullptr;void(* off)(void *)= nullptr;模板< Light T>静态constexpr PolyLightVtable make(){使用Light :: on;使用Light :: off;返回 {[](void * p){on(light_tag {},* static_cast< T *>(p));;},[](void * p){off(light_tag {},* static_cast< T *>(p));}};}模板< Light T>静态PolyLightVtable const * get(){静态constexpr auto retval = make< T>();返回和撤回}};struct PolyLightRef {PolyLightVtable const * vtable = 0;void *状态= 0;无效on(){vtable-> on(州);}无效off(){vtable-> off(状态);}模板< Light T>要求(!std :: is_same_v< std :: decay_t< T>,PolyLightRef>)PolyLightRef(T& l):vtable(PolyLightVtable :: get< std :: decay_t< T>>()),状态(std :: addressof(l)){}};} 

现在我们可以写:

  void foo(Light :: PolyLightRef light){点亮();light.off();} 

,我们得到动态调度;可以从调用方中隐藏 foo 的定义.

PolyLightRef 扩展为 PolyLightValue 并不是那么棘手-我们只需将vstable中的Assign(move/copy)/construct(move/copy)/destroy添加,或者在 void * 中将状态填充到堆中,或者在某些情况下使用小型缓冲区优化.

现在,我们有了一个完整的具有动态特征"特征的防锈系统.基于特征的调度,在入口点测试特征(当您以 Light auto PolyLightYYY 传递特征时),在特征名称空间在类型的名称空间中,等等.

我个人很期待演绎指南可用于使 CompiletimePoly 的使用烦恼一些:

  LightRef ref =灯光; 

可以为您推断出 T

  template< class T>LightRef(T&)-> LightRef< T> ;; 

(可能是为您写的),以及在致电网站上

  LightRefTemplateTakingFunction(LightRef {foo}) 

包含错误消息的实时示例

I have come across the interesting and powerful concept of "traits" recently and am attempting to understand/implement them in C++. From what I understand, traits provide a way both extend/adapt the functionality of exiting code and define an "interface" for a class without using traditional inheritance (and all the overhead/problems that come with it). I also see that this concept seems to be closely related to the CRTP design pattern in C++.

As an example, my normal thought process for writing an interface in C++ would be to define a class with pure virtual methods. I can then create a subclass of this and pass a pointer to all my generic code. I am discovering that this has some problems however:

  1. Classes that need to inherit from multiple interfaces require the use of multiple inheritance which can become very complex and introduce the "diamond pattern" problem.
  2. A strict "is a" relationship is formed which is not always the intent. For example, if I am describing the interface to a Light, a Simulated Light is NOT really a Light, it merely has the "traits"/acts like a Light. What I mean is, the generic Light interface doesn't really have commonalities that an implementation needs to inherit, it merely defines how an implementation should behave.
  3. Virtual methods and inheritance allow full dynamic polymorphism which incurs unnecessary overhead. In most of my code, I would only ever be using a single implementation of an interface at a time and thus I don't need to dynamically choose the correct implementation, I just need to have the "users" of the interface be generic enough to all for different implementations.

Here is an example of a simple, traditional interface for a 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();
}

And (based upon the article here: https://chrisbranch.co.uk/2015/02/make-your-c-interfaces-trait-forward/) here is what I think is a "traits-based" implementation of the same concept:

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

I have a few questions about this:

  1. Because, to use traits like this, you (temporarily) create a new Light instance, is there memory overhead associated with this?
  2. Is there a more effective method to document that a particular class "implements" a given trait?
  3. The article mentions two methods of defining a "user" for an interface. I have shown both above. lightUser2 seems to be the most well-document (it explicitly states that the function requires some implementation of the Light trait), however it requires that the implementations be explicitly casted into a Light outside of the function. Is there method to both document the intent of the user and all an implementation to be passed in directly?

Thank You!

解决方案

That looks like an adapter, not a trait as is used in C++.

Traits in C++ is like std::numeric_limits or std::iterator_traits. It takes a type and returns some information about that type. The default implementation handles a certain number of cases, and you can specialize it to handle other cases.


The code he wrote has a few issues.

  1. In Rust, this is used for dynamic dispatch. The template version is not dynamic.

  2. C++ thrives on value types. With the embedded reference, this cannot be a value type.

  3. The checking is late, at duck typing time, and the errors show up in the trait code, not at the call site.

An alternative approach to this is to use free functions and concepts and ADL.

turn_light_on(foo) and turn_light_off(foo) can be defaulted and found via ADL allowing customization of existing types. If you want to avoid the "one namespace" problem you can include an interface tag.

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

then we have our OddLight:

namespace Odd {
  class OddLight {
  public:
    void set(bool state);
  };
}

we want it to be a Light, so we do this:

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

then

struct not_a_light{};

if we have test code:

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
}

note that the concept map:

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

can be defined in either namespace Odd or namespace Light.

If you want to extend this to dynamic dispatch, you'd have to manually write the type erasure.

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

now we can write:

void foo( Light::PolyLightRef light ) {
    light.on();
    light.off();
}

and we get dynamic dispatch; the definition of foo can be hidden from the caller.

Extending PolyLightRef to PolyLightValue isn't that tricky -- we just add in assign(move/copy)/construct(move/copy)/destroy to the vtable, and either stuff the state onto the heap in the void* or use small buffer optimization in some cases.

And now we have a full rust-esque system of dynamic "trait" based dispatch, the traits are tested at the point of ingress (when you pass them either as Light auto or PolyLightYYY), customization in either the trait namespace or in the type's namespace, etc.

I, personally, am looking forward to having reflection, and the possibility of automating some of the above boilerplate.


There is actually a grid of useful variants:

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

you can write to solve this problem in a Rust-like way.

deduction guides can be used to make the CompiletimePoly a bit less annoying to use:

LightRef ref = light;

can deduce T for you with

template<class T>
LightRef(T&)->LightRef<T>;

(this may be written for you), and at call site

LightRefTemplateTakingFunction( LightRef{foo} )

Live example with error messages

这篇关于了解C ++特性并使其高效的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆