C++概念需要模板的某些行为

41zrol4v  于 2023-06-25  发布在  其他
关注(0)|答案(3)|浏览(190)

我们如何使用一个概念来要求行为(例如是否存在某种方法)?
让我们举一个例子:

  1. template <typename T>
  2. concept HasFoo = requires(T t) {
  3. t.foo();
  4. };

现在我们有:

  1. struct X {
  2. void foo() {}
  3. };
  4. struct Y {};
  5. static_assert(HasFoo<X>); // ok, passes
  6. static_assert(!HasFoo<Y>); // ok, passes

如果我们加上:

  1. template<typename T>
  2. struct Z {
  3. auto foo() {
  4. return T().foo();
  5. }
  6. };

这与预期的一样:

  1. static_assert(HasFoo<Z<X>>); // passes

但以下两种情况编译失败:

  1. static_assert(HasFoo<Z<Y>>); // static assert fails
  2. static_assert(!HasFoo<Z<Y>>); // compilation error: no member named 'foo' in 'Y'

当静态Assert可以通过时,我们会因为没有'foo'而得到编译错误,这并没有多大帮助。是否有一种方法来实现这个概念,使它在这个案例中起作用?
这是第一个问题,模板似乎是为了检查概念而示例化的,这使编译失败。
Code link
如果我们稍微改变Z:

  1. template<typename T>
  2. struct Z {
  3. void foo() {
  4. T().foo();
  5. }
  6. };

然后编译器认为Z有foo,不管它的内部类型是否实现了foo,因此:

  1. static_assert(HasFoo<Z<Y>>); // passes
  2. static_assert(!HasFoo<Z<Y>>); // fails

这是第二个问题。(似乎没有简单的解决办法)。
Code link
这两个问题是一样的吗?或者没有?有没有可能一个解决方案,而另一个没有?
如何安全地实现代码like this

  1. template<typename T>
  2. void lets_foo(T t) {
  3. if constexpr(HasFoo<T>) {
  4. t.foo();
  5. }
  6. }

当模板化类型可能失败时:

  1. int main() {
  2. lets_foo(X{}); // ok, calls foo inside
  3. lets_foo(Y{}); // ok, doesn't call foo inside
  4. lets_foo(Z<X>{}); // ok, calls foo inside
  5. lets_foo(Z<Y>{}); // fails to compile :(
  6. }

注意:这个问题是一个基于类似但更具体的问题的后续,这个问题得到了具体的解决方案:How to do simple c++ concept has_eq - that works with std::pair (is std::pair operator== broken for C++20),看起来这个问题不仅仅是一个单一的问题。

v2g6jxz6

v2g6jxz61#

这里的主要问题是Zfoo()没有任何约束,但它的实现期望表达式T().foo()是良好格式的,这将在T没有foo()时在函数体内部导致硬错误,因为concept只检查函数的签名。
最直接的方法是约束Z::foo()以符合其内部实现(尽管这也要求T是默认可构造的)。

  1. template<typename T>
  2. struct Z {
  3. auto foo() requires HasFoo<T> {
  4. T().foo();
  5. }
  6. };
qcuzuvrc

qcuzuvrc2#

根据@PatrickRoberts的评论,我们不能SFINAE模板或检查函数的存在,其中存在依赖于模板的内部实现,这可能会导致编译错误,因为需要解析模板,这可能会遇到会导致硬错误的替换失败,或者是一个错误的答案--在问题结尾的if constexpr的意义上是错误的。
看起来,唯一能把我们从上面拯救出来的是实现模板本身的人。
例如,我们可以如下实现Z:

  1. template <typename T>
  2. concept HasFoo = requires(T t) {
  3. t.foo();
  4. };
  5. template<typename T>
  6. struct Z {};
  7. template<HasFoo T>
  8. struct Z<T> {
  9. void foo() {
  10. T().foo();
  11. }
  12. };

现在我们可以安全地使用Z:

  1. struct X {
  2. void foo() {}
  3. };
  4. struct Y {};
  5. static_assert(HasFoo<X>);
  6. static_assert(!HasFoo<Y>);
  7. // static_assert(HasFoo<Z<Y>>); // would fail, justifiably
  8. static_assert(!HasFoo<Z<Y>>); // passes
  9. template<typename T>
  10. void lets_foo(T t) {
  11. if constexpr(HasFoo<T>) {
  12. t.foo();
  13. }
  14. }
  15. int main() {
  16. lets_foo(X{}); // ok, calls foo inside
  17. lets_foo(Y{}); // ok, doesn't call foo inside
  18. lets_foo(Z<X>{}); // ok, calls foo inside
  19. lets_foo(Z<Y>{}); // ok, doesn't call foo inside
  20. }

Code link
现在,如果我们想查询多个成员,会发生什么?我们也许应该使用Mixin,就像这样:

  1. template<HasFoo T>
  2. struct Fooable {
  3. void foo() {
  4. T().foo();
  5. }
  6. };
  7. template<HasMoo T>
  8. struct Mooable {
  9. void moo() {
  10. T().moo();
  11. }
  12. };
  13. template<typename T>
  14. struct Z {};
  15. template<HasFoo T>
  16. struct Z<T>: Fooable<T> {};
  17. template<HasMoo T>
  18. struct Z<T>: Mooable<T> {};
  19. template<typename T>
  20. requires HasFoo<T> && HasMoo<T>
  21. struct Z<T>: Fooable<T>, Mooable<T> {};

现在允许:

  1. template<typename T>
  2. void lets_foo_and_moo(T t) {
  3. if constexpr(HasFoo<T>) {
  4. t.foo();
  5. }
  6. if constexpr(HasMoo<T>) {
  7. t.moo();
  8. }
  9. }
  10. int main() {
  11. lets_foo_and_moo(X{}); // ok, calls foo inside
  12. lets_foo_and_moo(Y{}); // ok, calls nothing
  13. lets_foo_and_moo(Z<X>{}); // ok, calls foo inside
  14. lets_foo_and_moo(Z<Y>{}); // ok, calls nothing
  15. }

Code link
无论如何,负担都在模板实现者身上,假设他们知道这个需求。
在发布了我的答案之后,我现在看到了the solution proposed by @康桓瑋,我必须说它更简单和优雅(尽管Mixin方法在某些情况下可能有用)。

展开查看全部
kxeu7u2r

kxeu7u2r3#

C++不要求编译器在替换失败发生之前完全示例化所有代码。
这是因为编译器编写者认为这样做很难。
通常,方法的主体内的失败会导致硬编译时错误。在不修改方法本身的情况下,不存在检测此类故障的方法,并且这是有意的。在编译方法体时检测到错误,编译可以立即停止,没有回退。
当SFINAE(替换失败不是错误)是唯一的实现这种事情的技术时,没有失败硬的方法被称为“SFINAE友好”。
在概念时代,它看起来像:

  1. void foo() requires HasFoo<T> {

而不是

  1. void foo() {

预先概念,您可以进行手动检查,或使用宏,如:

  1. #define RETURNS(...) -> decltype(__VA_ARGS__) { return __VA_ARGS__; }

然后做

  1. auto foo() RETURNS( T().foo() }

它做了一些类似的事情。
这个问题-方法中的硬错误-发生在std库中。例如,vectoroperator<被定义为在其包含的值上简单地调用<-如果失败,则错误是硬错误。
我在过去用per-std-container专门化来解决这个问题,它检测所包含类型的需求并(手动)推导容器属性。
避免这种手工工作的唯一方法是与你交互的类型进行合作。

展开查看全部

相关问题