一、concepts的入门应用
concepts的应用是一个非常必要的问题。它对于模板在实际编程中的友好性有着至关重要的作用。先从最简单的一个示例说起:
struct PlusSum
{
int d_ = 9;
double a_ = 11;
};
template <typename T,typename N>
concept C2 = requires(T t, N n) {
{t + n}-> std::same_as<T>;//要求t+n是T类型
};
template <typename T, typename N>
concept C1 = requires(T t, N n) {
{t + n}-> std::convertible_to<T>;//要求t+n可转成T类型
};
template<typename T,typename N>
T Sum(T t, N n) requires C2<T,N>
{
return t + n;
}
int main()
{
int d = 10;
double a = 9;
//在这种情况double+int=>double所以C2不满足
int r = Sum(d ,a);
std::cout << "sum value:" << r << std::endl;
//无法满足约束
r = Sum(PlusSum{}, PlusSum{});
std::cout << "sum value :" << r << std::endl;
}
如果在这个函数里把概念部分先去除,编译器只会报“未匹配的重载函数”。如果把C1打开,则会报约束无法满足,但此时只是第个Sum有问题,第一个没有问题。如果再把C2打开,把C1注释,会发现两个Sum都说不满足约束。换句话说,两个约束一个比一个更严格,所以在某些场景下,对类型要求严格时,可以做类似的判断。
在这样一个简单的例程中,当然显示不出来约束的巨大优势。毕竟就几行代码,一扫而过也能找到错误。但是如果在实际工程中,不断嵌套调用,如何一下子定位出来呢?甚至像那些非严格自动转化的类型引起的警告导致的错误,可能会让开发者寻找一天。这时候儿用约束的优势就明白的显现出来了。
二、更进一步的应用
在上面只是简单的使用了一下requires,下面综合的对一些成员函数,CV限定等进行控制,看一下代码:
//函数限制
template<typename T>
concept func_ask = requires(T t)
{
t.must();
};
class A {};
class B
{
public:
void must() const {}
};
class C : public A
{
public:
void must(const std::string& s = "test") const { std::cout << s << std::endl; }
};
//下面的继承改成私有亦会报错
class D :public B//private B
{
public:
D() {};
~D() {};
private:
};
template <typename T>
concept is_const_ok = !std::is_const_v<T>;
template <typename T>
concept is_size_ok = requires (T t)//有size()函数且返回值可转size_t
{
{t.size()}->std::convertible_to<std::size_t>;
};
template<typename T,typename N>
concept cs1 = requires (T t,N n)
{
{t.Get(n)}->std::same_as<std::string>;
};
template <typename T>
class OkTest
{
public:
/*int*/std::string Get(int d) { std::cout << "is test" << std::endl; return ""; }//返回值限制,可以试一下
};
//带参数限制
template <typename T,typename N>
requires cs1<T,N>
void TestOK(T t,N n)
{
t.Get(n);
}
template<typename T>
concept cs = requires (T t)
{
// {t}->std::same_as<std::string>;//约束通不过,估计是引用常量啥的问题
{std::decay_t<T>(t)}-> std::same_as<std::string>;
//requires requires {std::is_same_v<decltype(t), decltype("123")>; };//这个约束也可以,不过有点LOW
//t.size() < 6;//这个约束没有用,可以考虑一下为什么
};
template <typename T>
concept cf = requires (T t)
{
std::is_function_v<decltype(t) >;
};
template <cs T>
auto DataGet( T &t)
{
return t;
}
template<typename T, cf F >
class MyConceptsExample
{
public:
MyConceptsExample() {}
~MyConceptsExample() {}
template <cs U>
U SetName(U t)
{
s_ = t;
std::cout << "get name is:" << s_ << std::endl;
return s_;
}
int size()
{
return 10;
}
std::string s_ = "";
F f_;
};
template<func_ask T>
requires is_const_ok<T> //简化了std::enable_if的应用,引用永远是flase=>true
void must(const T& t)
{
t.must();
}
template<typename T>
requires is_size_ok<T>
auto createEx(T& t)
{
std::cout << "size value is:" << t.size() << std::endl;
}
#include<functional>
typedef void(*PF)(int);
void IsFunc(int d)
{
std::cout << d << std::endl;
}
void TestMyConcepts()
{
OkTest<int > ok;
TestOK(ok,10);
std::string s = "123";
int d = 0;
DataGet(s);//换成d,就会报约束问题
MyConceptsExample<int, std::function<void(int)>> my;
MyConceptsExample<int, PF> my1;
// my.SetName(s);
createEx(my);
//must(A()); // 未满足关联约束
must(B());
must(C());
must(D());
return;
}
int main()
{
TestMyConcepts();
return 0;
}
基本按上面的代码,就可以把需要概念应用上来。这样,在实际开发中,对一些细节的掌控就可以通过概念来控制。比起以前的SFINAE要简单方便易理解多了。特别是在c++20中好多原来SFINAE中的技巧都可以直接使用了,所以说技术还是得进步,光靠技术人员玩一些花活,不是发展之道。解决一时之需还可,长久来看,反而对c++语言的发展有碍。
三、更高的一些用法
分析完了一些基本的用法,来看一些高级的用法。
1、模板的特化
在模板的使用中,使用了concepts后,模板的特化和偏特化不受影响,但在此过程中必须符合约束的要求:
#include <concepts>
#include <string>
#include <type_traits>
template <std::signed_integral T>
class NewData
{
public:
T GetData(T t) { return t; }
};
template <>
class NewData<long> {};
//template <>
//class NewData<std::string> {};//不满足约束
template <std::signed_integral T,typename U>
requires std::is_const_v<U>
class NewDataConst
{
public:
T GetData(T t,U u) { return t; }
};
template<typename U>
class NewDataConst<int, U> {};
template<typename U>
class NewDataConst<double, U> {};
template<typename T>
class NewDataConst<T,const int> {};
template<typename T>
class NewDataConst<T, const std::string> {};
int main()
{
NewData<int> nd;
//NewData <float> nd1;//不满足约束
//NewData <std::string> nd2;//不满足约束
NewData<long> nd3;
// NewDataConst<double, int> ndc;
NewDataConst<int, const double> ndcc;//double换成 std::string会有歧义
// NewDataConst<int, int> n;
return 0;
}
上面的代码会发现,全特化时约束立刻起作用,而偏特化要等到定义应用时才会起作用。当然concepts在一些情况下也可以特化模板:
template <typename T>
class Example
{
public:
Example() { std::cout << "stand " << std::endl; }
};
template <std::integral T>
class Example<T>
{
public:
Example() { std::cout << "no stand " << std::endl; }
};
void TestExample()
{
Example<int> t1; // no stand
Example<std::string> t2; // stand
}
int main()
{
TestExample();
return 0 ;
}
2、函数重载
那么如果有concepts的限制,函数重载有什么影响么?
void funcall(int d)
{
std::cout << "call stand func" << std::endl;
}
template<typename T>
void funcall(T t)
{
std::cout << "call template func" << std::endl;
}
template<std::integral T>
void funcall(T t)
{
std::cout << "call concept template func" << std::endl;
}
template <>
void funcall<double>(double d)
{
std::cout << "call all template func" << std::endl;
}
int main()
{
int d = 0;
funcall(d);
funcall("test");
funcall<>(1.1);
funcall<int>(1);
return 0;
}
结果:
call stand func
call template func
call all template func
call concept template func
通过上面的代码可以看到其实concepts是不影响函数重载的。
四、总结
古人形容朝代的兴亡用“其成也勃焉,其亡也忽焉”。其实,一个语言的兴衰也是如此。js 和python为什么能够迅速起来,简单是第一位。不要提什么功能强大,那都是基于简单的前提。所以c++必须得走一条在尽可能范围的情况下,把编写c++代码的难度降下来。这个不是谁说了算的问题,是涉及到一个语言能否存在的重要前提。
让我们拭目以待c++23以后的新标准会有什么更大惊喜出来。