C++20 类与接口编程初窥2 利用trait取代继承

XiLaiTL大约 7 分钟

C++20 类与接口编程初窥2 利用trait取代继承

要取代继承,需要取代继承提供的两件事——类型检测与方法重写/转型。

上一篇我们讲到,利用concept替代接口形式:

template<typename ClassName>
concept InterfaceName1 = requires(ClassName object) {
    {object.setName(string{})}->same_as<void>;
    {object.getName() }->same_as<string>;
};

利用InterfaceName1对类进行校验,校验通过,则证明类实现了接口所需要的函数。

然而,仅仅判断类中有哪些函数,并不能代表这个类实现了这个接口,也不能进行语义上的分发。为了区分语义,我们可以通过增添一个特殊的成员变量或者增添一个嵌套子类型来区分不同的concept。

template<typename ClassName>
concept InterfaceName1 = requires(ClassName object) {
    typename ClassName::InterfaceCommonName;
    {object.setName(string{})}->same_as<void>;
    {object.getName() }->same_as<string>;
};

当有类需要实现这个接口时,只要在类中有 using InterfaceCommonName = int;即可通过检测,而且认定这个类中的两个方法是实现InterfaceName1语义的,而不是实现其他同名函数接口的语义。

这样的缺点是:当需要同时实现两个接口时,会发生冲突。但是如C#,可以在类中选择方法是实现了哪个接口:

interface A{
	int GetValue();
}
interface B{
	int GetValue();
}
class Test:A,B{
	int A.GetValue(){ return 1;}
	int B.GetValue(){ return 2;}
}
//usage: ((A)test).GetValue();

在C++中,可以采用模板特化来实现这样的功能。

类模板特化实现 trait 的效果

From 夏の幻 akemimadoka (Natsu) (github.com)open in new window

我们可以定义一个泛型接口类ATraits,把方法的实现延期到特化的泛型类中ATraits<TestClass>。而对于类型的检测,仅仅是判断该类型是否实现了这个特化。这样把类和方法的调用分离了。

类型检测上:

只检测接口ATraits的特化能不能用在这个类型上。注意方法得传入self

	template <typename T> struct ATraits;
	template <typename T>
	concept ATraitsType = requires(T & self) {
		{ATraits<T>::GetValue(self)}->same_as<int>;
	};

	template <typename T> struct BTraits;
	template <typename T>
	concept BTraitsType = requires(T & self) {
		{BTraits<T>::GetValue(self)}->same_as<int>;
	};

	export auto testTrait(BTraitsType auto b) -> void {
		auto value = BTraits<decltype(b)>::GetValue(b);
		cout << value;
	}

方法的实现上:

对接口的特化

	template<>
	struct ATraits<TestClass>
	{
		static auto GetValue(TestClass& self) -> int { return 1; }
	};

	template<>
	struct BTraits<TestClass>
	{
		static auto GetValue(TestClass& self) -> int { return 2; }
	};

这样的优点是:把实现对应接口的代码整合到一块去,逻辑清晰。

缺点是:对象和方法不内联,单得知对象无法知道对象有该方法。在方法的调用中采用的形式不是 对象.方法()的形式,而是采用接口<类>::方法(对象)的形式。

完整代码

export module TestTrait1;

import<concepts>;
import<iostream>;
using std::same_as;
using std::cout, std::operator&, std::endl;

namespace TraitTest1 {
	template <typename T> struct ATraits;
	template <typename T>
	concept ATraitsType = requires(T & self) {
		{ATraits<T>::GetValue(self)}->same_as<int>;
	};

	template <typename T> struct BTraits;
	template <typename T>
	concept BTraitsType = requires(T & self) {
		{BTraits<T>::GetValue(self)}->same_as<int>;
	};

	export struct TestClass
	{};

	template<>
	struct ATraits<TestClass>
	{
		static auto GetValue(TestClass& self) -> int { return 1; }
	};

	template<>
	struct BTraits<TestClass>
	{
		static auto GetValue(TestClass& self) -> int { return 2; }
	};

	export auto testTrait(BTraitsType auto b) -> void {
		auto value = BTraits<decltype(b)>::GetValue(b);
		cout << value;
	}
}

模板特化的情况也被用于C++20 format库中。

如果需要为自定义的类实现format,就需要特化formatter。

为自定义类型提供C++20 Format库支持 - 知乎 (zhihu.com)open in new window

函数模板特化实现 trait 的效果

这里反过来,把接口作为模板的参数,让函数接收接口,从而分发不同效果。这里的模板就相当于一个标签tag,用于区别实现的接口的标签tag。

在类型检测中,我们只考虑被检测的类型能否调用接口名作为模板内容的方法。

struct ATraits;
template <typename T>
concept ATraitsType = requires(T & self) {
    {self.template GetValue<ATraits>()}->same_as<int>;
};

struct BTraits;
template <typename T>
concept BTraitsType = requires(T & self) {
    {self.template GetValue<BTraits>()}->same_as<int>;
};

auto testTrait(ATraitsType auto c) -> void {
    auto value = c.GetValue<ATraits>();
    cout << value << endl;
}

方法实现上,可以采用两种方式,都是把方法的实现写进类里,这是与上面那一种方式最大的不同:

全特化

struct TestATraits1{
    template<typename T>
    auto GetValue() -> int;
};

template<>
auto TestATraits1::GetValue<ATraits>() -> int{ return 9999; }

template<>
auto TestATraits1::GetValue<BTraits>() -> int{ return 6666; }

条件限制的特化

struct TestATraits2{
    template<same_as<ATraits> T>
    auto GetValue() -> int { return 1; }

    template<same_as<BTraits> T>
    auto GetValue() -> int { return 2; }
};

值得一提的是,TestATraits1可以通过任一要求GetValue<T>()的类型检测。而且有一种默认方法的效果。

如果两种方式都采用,则全特化的优先级会高于条件限制的特化。

这样的优点是:格式完整统一,脉络清晰,对象调用函数时可以根据实现的接口填写模板参数。

缺点是:TestATraits1和TestATraits2虽然都可以通过ATraitsType的类型检测,但是依然没办法向上转型为ATraits,无法进行动态分发等等。

实现动态分发

https://github.com/IFeelBloated/Type-System-Zoo/blob/master/existential type (multiple dispatch).cxxopen in new window

有些需求是:需要将子类型塞入接口类型的数组中,这个数组只完成接口定义的方法内容。

我们可以使用std:function对方法进行包装和转发。

struct ATraits{

    function<auto()->int> fGetValue = {};
    
    template<same_as<ATraits> T>
    auto GetValue() -> int { return fGetValue(); }

    ATraits(ATraitsType auto aTraits) {
        fGetValue = [&]()->int { return aTraits.GetValue<ATraits>(); };
    }

};

auto testVectorForATraits()->void{
    auto list = vector<ATraits>{ TestATraits1{},TestATraits2{} };
    for (auto& v : list) {
        cout<< v.GetValue<ATraits>()<< endl;
    }
}

这样也同时完成了向上转型,

完整代码:

import <concepts>;
import <iostream>;
import <vector>;
import <functional>;

using std::function;
using std::vector;
using std::same_as;
using std::cout, std::operator<<, std::endl;

struct ATraits;
template <typename T>
concept ATraitsType = requires(T & self) {
    {self.template GetValue<ATraits>()}->same_as<int>;
};

struct BTraits;
template <typename T>
concept BTraitsType = requires(T & self) {
    {self.template GetValue<BTraits>()}->same_as<int>;
};

struct ATraits{

    function<auto()->int> fGetValue = {};
    template<same_as<ATraits> T>
    auto GetValue() -> int { return fGetValue(); }

    ATraits(ATraitsType auto aTraits) {
        fGetValue = [&]()->int { return aTraits.GetValue<ATraits>(); };
    }

};

struct TestATraits1{
    template<typename T>
    auto GetValue() -> int;
};

template<>
auto TestATraits1::GetValue<ATraits>() -> int{
    return 9999;
}

template<>
auto TestATraits1::GetValue<BTraits>() -> int{
    return 6666;
}

struct TestATraits2{
    template<same_as<ATraits> T>
    auto GetValue() -> int { return 1; }

    template<same_as<BTraits> T>
    auto GetValue() -> int { return 2; }
};



auto testTrait(ATraitsType auto c) -> void {
    auto value = c.GetValue<ATraits>();
    cout << value << endl;
}

auto main()->int{

    auto testClass = TestATraits1{};
    testTrait(testClass);

    cout<< testClass.GetValue<BTraits>();

    auto list = vector<ATraits>{ TestATraits1{},TestATraits2{} };
    for (auto& v : list) {
        cout<< v.GetValue<ATraits>()<< endl;
    }

    return 0;
}

宏定义出语法糖

这节可以跳过,纯属我自嗨。

可以发现,如果要抛开虚基类实现继承,要用到这么多的噪声,代码上太丑陋了。

这些语法都具有一定的格式,于是我想可以用宏定义去替换它。

例如:

#define trait(x) \
struct x;\
template <typename T> \
concept x##Type = requires(x type, T & self)

#define _let(x) {self.x
#define _fun(x)  {self.template x<decltype(type)>
#define _as }->same_as

#define impl(x) template<same_as<x> T>
trait(ATraits){
	_let(name) _as <string>;
	_fun(getName)() _as <string>;
};

struct TestClass{
	string name;
	
	impl(ATraits)
	auto getName() -> string { return name; }
};

是不是有种feel倍儿爽?

于是我再更进一步统一一下语法,把各种定义写成 关键字(名称,类型)后缀

TestDefine.h

#pragma once

#define trait(x) \
struct x;\
template <typename T> \
concept x##Type = requires(x type, T & self)

#define _let(x,...) {self.x}->same_as<__VA_ARGS__&>
#define _fun(x,y,...)  {self.template x<decltype(type)>__VA_ARGS__}->same_as<y>

#define impl(x) template<same_as<x> T>
#define implall template<typename T>
#define over_fun(t,x,y,...) \
template<>\
auto x<t> __VA_ARGS__ -> y

#define init(x) typedef BTraits TRAITNAME; x (x##Type auto self)
#define fun_init(x,y,...) f##x = [&] __VA_ARGS__ ->y  { return self.template x<TRAITNAME> 
#define fun_(x,y,...) \
private:\
function<auto __VA_ARGS__ -> y > f##x = {}; \
public:\
template<same_as<TRAITNAME> T> \
auto x __VA_ARGS__  -> y { \
return f##x
#define doit(...) (__VA_ARGS__); };

#define let(x,...) __VA_ARGS__ x
#define fun(x,y,...) auto x __VA_ARGS__ -> y
#define var(x,...) auto x = __VA_ARGS__
#define val(x,...) const auto x = __VA_ARGS__
#define lam(x,y,...) auto x = __VA_ARGS__ -> y
#define data(x) struct x
#define is(x) = x
#define call(x) template x
#define with(...) template<__VA_ARGS__>
#define arg(x,...) __VA_ARGS__ x
#define _arg(x,y) y##Type auto x
#define type(x) typename x
#define con(x) concept x
#define need(x) requires x
module;
#include"TestDefine.h"

export module TestDefine;

import<functional>;
import<concepts>;
import<iostream>;
import<vector>;
import<array>;
using std::function;
using std::array;
using std::vector;
using std::same_as;
using std::cout, std::operator<<, std::endl;

trait(BTraits) {
	_let(y, int);
	_fun(getValue,int,());
};

export
data(BTraits) {
	init(BTraits) {
		fun_init(getValue,int,()) doit();
	}
	fun_(getValue, int, ()) doit();
};

export 
data(TestClass){
	let(y,int);

	impl(BTraits)
	fun(getValue, int, ()) {
		return 1;
	};
};

export
data(TestClass2) {
	let(y, int);

	implall
	fun(getValue, int, ()) {
		return 3;
	};
};

over_fun(BTraits, TestClass2::getValue, int, ()) {
	return 0;
};

export
fun(testDefine, void, (_arg(x, BTraits))) {
	val(y, x.getValue<BTraits>());
	val(e, static_cast<int>(15));
	lam(p, int, [e](arg(x, int))) { return x + e; };
	val(z, array<int, 10>) { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	for (arg(p, auto) : z) {
		cout << y + p<<endl;
	};
	cout << p(16) << endl;
};

export
with(type(T), arg(C, same_as<int>))
fun(testDefine2, void, (arg(x, T), arg(y, C))) {
	val(l, vector<BTraits>) { TestClass{}, TestClass2{} };
	for (arg(v, auto) : l) {
		cout << v.getValue<BTraits>() + x << endl;
	};
};
上次编辑于:
贡献者: XiLaiTL