Mq-b / Modern-Cpp-templates-tutorial

现代C++模板教程
https://mq-b.github.io/Modern-Cpp-templates-tutorial/
Other
662 stars 88 forks source link

关于约束的一个问题 #35

Closed YiRanMushroom closed 7 months ago

YiRanMushroom commented 7 months ago

我在尝试写一个运算符重载时遇到问题,我将代码简化后这是可以表现出问题的代码:

template<typename value_type>
    std::ostream &operator<<(std::ostream &os, const std::vector<value_type> &vec) requires requires(value_type val)
{
    std::cout << val;
} {
    // ...
    return os;
}

在进行测试的时候发现一维std::vector正常:

std::vector vec{1, 2, 3, 4, 5};
std::cout << vec;
// 可以通过编译

但是std::vector<std::vector>无法通过编译。

std::vector<std::vector<int> > vec2{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
std::cout << vec2;
// 编译失败

这个问题可以通过其他复杂的方式解决,我就是不明白为什么这个约束会不满足,这个模板推断应该是可以在编译期递归推断的,不明白这里为什么不行。

Mq-b commented 7 months ago
template<typename value_type>
   std::ostream &operator<<(std::ostream &os, const std::vector<value_type> &vec) >requires requires(value_type val)
{
   std::cout << val;
} {
   // ...
   return os;
}
  1. 你的代码格式太糟糕了,不清楚是你真的这样写,还是发布的时候变成这样。
  2. 你应该使用 https://godbolt.org/ 发布你的测试 demo,最次也应该说明环境和编译器编译选项。
  3. 实测 MSVC 可以通过编译,gcc13.2 和 clang17.0 不行,个人认为这是个编译器 bughttps://godbolt.org/z/6Yc5GfeT3
  4. 切换至最新版同样不可:https://godbolt.org/z/PWvjqjdWf
Mq-b commented 7 months ago

改成:

template<class T>
std::ostream& operator<<(std::ostream&, const std::vector<T>&);

template<typename value_type>
requires requires(value_type val) {
    std::cout << val;
} || requires(value_type v) { ::operator<<(std::cout, v); }
std::ostream& operator<<(std::ostream& os, const std::vector<value_type>& vec){
    for(const auto& val : vec){
        os << val << " ";
    }
    std::cout << std::endl;
    return os;
}

可以通过编译,似乎是名字查找或重载决议选择存在问题,添加向前声明析取的约束才可以,,,

Mq-b commented 7 months ago

我看到一篇似乎和这个有一点关系的文章(不确定)。

@frederick-vs-ja

frederick-vs-ja commented 7 months ago

从报错来看原因很明显是在确定约束处这个重载本身不可见。

此处重载还未被声明,于是通常名字查找不会找到此重载。然后由于所有参数的关联命名空间只有 std, ADL 也不会找到全局命名空间中的重载。

解决方案:避免依赖可能未声明的 operator<<,用变量模板的特化进行元递归。

template<typename T>
constexpr bool can_be_output_by_ostream = requires(std::ostream& os, T val) {
    os << val;
};
template<typename T, typename A>
constexpr bool can_be_output_by_ostream<std::vector<T, A>> = can_be_output_by_ostream<T>;

template<typename T, typename A>
    requires can_be_output_by_ostream<T>
std::ostream& operator<<(std::ostream& os, const std::vector<T, A>& vec)
{
    for(const auto& val : vec){
        os << val << " ";
    }
    os << std::endl;
    return os;
}

测试样例: https://godbolt.org/z/rvbo5n8Kv

当然更理想的方向是不要试图对这种只涉及标准库类型和基础类型的东西添加重载,只对自己定义的类或枚举添加重载。

Mq-b commented 7 months ago

那么结论是?MSVC 的行为不符合标准,还是 gcc 与 clang?

frederick-vs-ja commented 7 months ago

那么结论是?MSVC 的行为不符合标准,还是 gcc 与 clang?

应该是 MSVC 的行为存在问题。

Mq-b commented 7 months ago

那么结论是?MSVC 的行为不符合标准,还是 gcc 与 clang?

应该是 MSVC 的行为存在问题。

嗯,,,只是这个还是反直觉了,C++ 的规则,唉。这种情况不如直接不添加 requires 约束,直接通过编译。