concepts的应用是一个非常必要的问题。它对于模板在实际编程中的友好性有着至关重要的作用。先从最简单的一个示例说起:
struct PlusSum
{
int d_ = 9;
double a_ = 11;
};
template
concept C2 = requires(T t, N n) {
{t + n}-> std::same_as;//要求t+n是T类型
};
template
concept C1 = requires(T t, N n) {
{t + n}-> std::convertible_to;//要求t+n可转成T类型
};
template
T Sum(T t, N n) requires C2
{
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
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
concept is_const_ok = !std::is_const_v;
template
concept is_size_ok = requires (T t)//有size()函数且返回值可转size_t
{
{t.size()}->std::convertible_to;
};
template
concept cs1 = requires (T t,N n)
{
{t.Get(n)}->std::same_as;
};
template
class OkTest
{
public:
/*int*/std::string Get(int d) { std::cout << "is test" << std::endl; return ""; }//返回值限制,可以试一下
};
//带参数限制
template
requires cs1
void TestOK(T t,N n)
{
t.Get(n);
}
template
concept cs = requires (T t)
{
// {t}->std::same_as;//约束通不过,估计是引用常量啥的问题
{std::decay_t(t)}-> std::same_as;
//requires requires {std::is_same_v; };//这个约束也可以,不过有点LOW
//t.size() < 6;//这个约束没有用,可以考虑一下为什么
};
template
concept cf = requires (T t)
{
std::is_function_v;
};
template
auto DataGet( T &t)
{
return t;
}
template
class MyConceptsExample
{
public:
MyConceptsExample() {}
~MyConceptsExample() {}
template
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
requires is_const_ok //简化了std::enable_if的应用,引用永远是flase=>true
void must(const T& t)
{
t.must();
}
template
requires is_size_ok
auto createEx(T& t)
{
std::cout << "size value is:" << t.size() << std::endl;
}
#include
typedef void(*PF)(int);
void IsFunc(int d)
{
std::cout << d << std::endl;
}
void TestMyConcepts()
{
OkTest ok;
TestOK(ok,10);
std::string s = "123";
int d = 0;
DataGet(s);//换成d,就会报约束问题
MyConceptsExample> my;
MyConceptsExample 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
#include
#include
template
class NewData
{
public:
T GetData(T t) { return t; }
};
template <>
class NewData {};
//template <>
//class NewData {};//不满足约束
template
requires std::is_const_v
class NewDataConst
{
public:
T GetData(T t,U u) { return t; }
};
template
class NewDataConst {};
template
class NewDataConst {};
template
class NewDataConst {};
template
class NewDataConst {};
int main()
{
NewData nd;
//NewData nd1;//不满足约束
//NewData nd2;//不满足约束
NewData nd3;
// NewDataConst ndc;
NewDataConst ndcc;//double换成 std::string会有歧义
// NewDataConst n;
return 0;
}
上面的代码会发现,全特化时约束立刻起作用,而偏特化要等到定义应用时才会起作用。当然concepts在一些情况下也可以特化模板:
template
class Example
{
public:
Example() { std::cout << "stand " << std::endl; }
};
template
class Example
{
public:
Example() { std::cout << "no stand " << std::endl; }
};
void TestExample()
{
Example t1; // no stand
Example t2; // stand
}
int main()
{
TestExample();
return 0 ;
}
2、函数重载
那么如果有concepts的限制,函数重载有什么影响么?
void funcall(int d)
{
std::cout << "call stand func" << std::endl;
}
template
void funcall(T t)
{
std::cout << "call template func" << std::endl;
}
template
void funcall(T t)
{
std::cout << "call concept template func" << std::endl;
}
template <>
void funcall(double d)
{
std::cout << "call all template func" << std::endl;
}
int main()
{
int d = 0;
funcall(d);
funcall("test");
funcall<>(1.1);
funcall(1);
return 0;
}
结果:
call stand func
call template func
call all template func
call concept template func
通过上面的代码可以看到其实concepts是不影响函数重载的。
古人形容朝代的兴亡用“其成也勃焉,其亡也忽焉”。其实,一个语言的兴衰也是如此。js 和python为什么能够迅速起来,简单是第一位。不要提什么功能强大,那都是基于简单的前提。所以c++必须得走一条在尽可能范围的情况下,把编写c++代码的难度降下来。这个不是谁说了算的问题,是涉及到一个语言能否存在的重要前提。
让我们拭目以待c++23以后的新标准会有什么更大惊喜出来。