43

令我惊讶的是,我遇到了另一个障碍,比如C++20 行为用相等运算符破坏现有代码?.

考虑一个简单的不区分大小写的键类型,与例如std::setor一起使用std::map

// Represents case insensitive keys
struct CiKey : std::string {
    using std::string::string;
    using std::string::operator=;

    bool operator<(CiKey const& other) const {
        return boost::ilexicographical_compare(*this, other);
    }
};

简单测试:

using KeySet   = std::set<CiKey>;
using Mapping  = std::pair<CiKey, int>; // Same with std::tuple
using Mappings = std::set<Mapping>;

int main()
{
    KeySet keys { "one", "two", "ONE", "three" };
    Mappings mappings {
        { "one", 1 }, { "two", 2 }, { "ONE", 1 }, { "three", 3 }
    };

    assert(keys.size() == 3);
    assert(mappings.size() == 3);
}

明显的解决方法

一个明显的解决方法是operator<=>在 C++20 模式下有条件地提供:编译资源管理器

#if defined(__cpp_lib_three_way_comparison)
    std::weak_ordering operator<=>(CiKey const& other) const {
        if (boost::ilexicographical_compare(*this, other)) {
            return std::weak_ordering::less;
        } else if (boost::ilexicographical_compare(other, *this)) {
            return std::weak_ordering::less;
        }
        return std::weak_ordering::equivalent;
    }
#endif

问题

令我惊讶的是,我遇到了另一种破坏性更改的情况——C++20 在没有诊断的情况下更改了代码的行为。

在我阅读std::tuple::operator<它时应该有效:

3-6)按字典顺序比较lhs,即比较第一个元素,如果它们等价,则比较第二个元素,如果它们等价,则比较第三个元素,依此类推。对于非空元组,(3) 等价于rhsoperator<

if (std::get<0>(lhs) < std::get<0>(rhs)) return true;
if (std::get<0>(rhs) < std::get<0>(lhs)) return false;
if (std::get<1>(lhs) < std::get<1>(rhs)) return true;
if (std::get<1>(rhs) < std::get<1>(lhs)) return false;
...
return std::get<N - 1>(lhs) < std::get<N - 1>(rhs);

我知道从技术上讲,这些从 C++20 开始就不再适用,它被替换为:

通过综合三向比较(见下文)按字典顺序进行比较lhsrhs即比较第一个元素,如果它们相等,比较第二个元素,如果它们相等,比较第三个元素,依此类推

和...一起

<、<=、>、>= 和 != 运算符分别由operator<=>和合成operator==(C++20 起)

事情是,

  • 我的类型没有定义operator<=>也不operator==

  • 并且正如这个答案所指出的那样,提供operator<额外的东西会很好,并且应该在评估简单的表达式时使用,比如a < b.

  1. C++20 中的行为更改是否正确/故意?
  2. 应该有诊断吗?
  3. 我们可以使用其他工具来发现像这样的无声破损吗?感觉就像扫描整个代码库以在tuple/中使用用户定义的类型pair并不能很好地扩展。
  4. tuple除了/之外,还有其他类型pair可以表现出类似的变化吗?
4

3 回答 3

47

基本问题来自于您的类型不连贯并且标准库直到 C++20 才调用您的事实。也就是说,你的类型总是有点坏,但事情的定义足够狭隘,你可以侥幸逃脱。

您的类型已损坏,因为它的比较运算符没有意义。它宣称它是完全可比较的,并定义了所有可用的比较运算符。发生这种情况是因为您公开继承自std::string,因此您的类型通过隐式转换为基类来继承这些运算符。但是这种比较表的行为是不正确的,因为您只替换了其中一个比较与其他比较不起作用。

而且由于行为不一致,一旦 C++ 真正关心你的一致性,可能发生的事情就可以解决了。

然而,一个更大的问题是与标准如何处理的不一致operator<=>

C++ 语言的设计目的是在使用综合运算符之前优先考虑显式定义的比较运算符。因此,如果您直接比较它们,您继承的类型std::string将使用您的类型。operator<

然而,C++ 库有时会尝试变得聪明。

某些类型尝试转发给定类型提供的运算符,例如optional<T>. 它被设计成与T它的可比性相同,并且在这方面取得了成功。

但是,pairtuple尝试变得有点聪明。在 C++17 中,这些类型实际上从未转发比较行为;相反,它根据类型的现有定义和定义综合了比较行为。operator<operator==

因此,他们的 C++20 版本延续了综合比较的优良传统也就不足为奇了。当然,由于该语言进入了该游戏,C++20 版本决定最好遵循他们的规则。

除了......它不能完全跟随他们。无法检测<比较是综合的还是用户提供的。因此,无法以其中一种类型实现语言行为。但是,您可以检测到存在三向比较行为。

所以他们做了一个假设:如果你的类型是三向可比的,那么你的类型依赖于综合运算符(如果不是,它使用旧方法的改进形式)。这是正确的假设;毕竟,既然<=>是一个新特性,旧类型不可能得到一个。

当然,除非旧类型继承自获得三向可比性的新类型。并且类型也无法检测到这一点。它要么是三向可比的,要么不是。

现在幸运的是,如果您的类型不提供三向比较功能,则pair和的综合三向比较运算符tuple完全能够模仿 C++17 的行为。因此,您可以通过删除重载显式地取消继承 C++20 中的三向比较运算符来恢复旧行为。operator<=>

或者,您可以使用私有继承并简单地公开using您想要的特定 API。

c++20 中的行为变化是否正确/故意?

这取决于您所说的“故意”。

公开继承 like 类型std::string在道德上一直有些可疑。与其说是因为切片/析构函数问题,不如说是因为它有点作弊。直接继承此类类型会导致 API 发生您未预料到且可能不适合您的类型的更改。

新的比较版本pairtuple正在做他们的工作,并且在 C++ 允许的范围内做到最好。只是你的类型继承了它不想要的东西。如果您私下继承std::string并仅using公开了您想要的功能,那么您的类型可能会很好。

应该有诊断吗?

这无法在某些编译器内在之外进行诊断。

我们可以使用其他工具来发现像这样的无声破损吗?

搜索您从标准库类型公开继承的情况。

于 2021-03-05T18:13:13.867 回答
12

啊! @StoryTeller 用他们的评论指出了这一点:

“我的类型没有定义 operator<=> 也没有 operator==" - 但是std::string确实,由于 d[e]rived-to-base 转换,使它成为候选者。我相信所有支持比较的标准库类型都对其成员进行了大修。

事实上,一个更快的解决方法是:

#if defined(__cpp_lib_three_way_comparison)
    std::weak_ordering operator<=>(
        CiKey const&) const = delete;
#endif

成功!编译器资源管理器

更好的想法

更好的解决方案,正如 StoryTeller 的第二条评论所暗示的:

我想非虚拟析构函数不再是避免从标准库容器继承的唯一令人信服的理由:/

将在这里避免继承:

// represents case insensiive keys
struct CiKey {
    std::string _value;

    bool operator<(CiKey const& other) const {
        return boost::ilexicographical_compare(_value, other._value);
    }
};

当然,这需要对使用代码进行(一些)下游更改,但它在概念上更纯粹,并且在未来与这种类型的“标准蠕变”绝缘。

编译器资源管理器

#include <boost/algorithm/string.hpp>
#include <iostream>
#include <set>
#include <version>

// represents case insensiive keys
struct CiKey {
    std::string _value;

    bool operator<(CiKey const& other) const {
        return boost::ilexicographical_compare(_value, other._value);
    }
};

using KeySet   = std::set<CiKey>;
using Mapping  = std::tuple<CiKey, int>;
using Mappings = std::set<Mapping>;

int main()
{
    KeySet keys { { "one" }, { "two" }, { "ONE" }, { "three" } };
    Mappings mappings { { { "one" }, 1 }, { { "two" }, 2 }, { { "ONE" }, 1 },
        { { "three" }, 3 } };

    assert(keys.size() == 3);
    assert(mappings.size() == 3);
}

剩下的问题

我们如何诊断这些问题。它们是如此微妙,以至于它们逃避代码审查。由于标准 C++ 存在 2 个十年,这种情况变得非常好并且可以预测。

我想作为旁注,当与从标准库类型继承过多的用户定义类型一起使用时,我们可以预期任何“提升”的运算符(考虑 std::variant/std::optional)都会有类似的缺陷。

于 2021-03-05T18:13:46.870 回答
3

这并不是对不同行为的真正答案std::string::operator=(),但我必须指出,创建不区分大小写的字符串应该通过自定义模板参数来完成Traits

例子:

// definition of basic_string:
template<
    class CharT,
    class Traits = std::char_traits<CharT>,   // <- this is the customization point.
    class Allocator = std::allocator<CharT>
> class basic_string;

不区分大小写字符串的示例几乎直接来自 cppreference ( https://en.cppreference.com/w/cpp/string/char_traits )。我using为不区分大小写的字符串添加了指令。

#include <cctype>
#include <cwctype>
#include <iostream>
#include <locale>
#include <string>
#include <version>

template <typename CharT> struct ci_traits : public std::char_traits<CharT>
{
    #ifdef __cpp_lib_constexpr_char_traits
    #define CICE constexpr
    #endif

private:
    using base = std::char_traits<CharT>;
    using int_type = typename base::int_type;

    static CICE CharT to_upper(CharT ch)
    {
        if constexpr (sizeof(CharT) == 1)
            return std::toupper(static_cast<unsigned char>(ch));
        else
            return std::toupper(CharT(ch & 0xFFFF), std::locale{});
    }

public:
    using base::to_int_type;
    using base::to_char_type;

    static CICE bool eq(CharT c1, CharT c2)
    {
        return to_upper(c1) == to_upper(c2);
    }
    static CICE bool lt(CharT c1, CharT c2)
    {
        return to_upper(c1) < to_upper(c2);
    }
    static CICE bool eq_int_type(const int_type& c1, const int_type& c2)
    {
        return to_upper(to_char_type(c1)) == to_upper(to_char_type(c2));
    }
    static CICE int compare(const CharT *s1, const CharT *s2, std::size_t n)
    {
        while (n-- != 0)
        {
            if (to_upper(*s1) < to_upper(*s2))
                return -1;
            if (to_upper(*s1) > to_upper(*s2))
                return 1;
            ++s1;
            ++s2;
        }
        return 0;
    }
    static CICE const CharT *find(const CharT *s, std::size_t n, CharT a)
    {
        auto const ua(to_upper(a));
        while (n-- != 0) {
            if (to_upper(*s) == ua)
                return s;
            s++;
        }
        return nullptr;
    }
    #undef CICE
};

using ci_string = std::basic_string<char, ci_traits<char>>;
using ci_wstring = std::basic_string<wchar_t, ci_traits<wchar_t>>;

// TODO consider constexpr support
template <typename CharT, typename Alloc>
inline std::basic_string<CharT, std::char_traits<CharT>, Alloc> string_cast(
    const std::basic_string<CharT, ci_traits<CharT>, Alloc> &src)
{
    return std::basic_string<CharT, std::char_traits<CharT>, Alloc>{
        src.begin(), src.end(), src.get_allocator()};
}

template <typename CharT, typename Alloc>
inline std::basic_string<CharT, ci_traits<CharT>, Alloc> ci_string_cast(
    const std::basic_string<CharT, std::char_traits<CharT>, Alloc> &src)
{
    return std::basic_string<CharT, ci_traits<CharT>>{src.begin(), src.end(),
                                                    src.get_allocator()};
}

int main(int argc, char**) {
    if (argc<=1)
    {
        std::cout << "char\n";
        ci_string hello = "hello";
        ci_string Hello = "Hello";

        // convert a ci_string to a std::string
        std::string x = string_cast(hello);

        // convert a std::string to a ci_string
        auto ci_hello = ci_string_cast(x);

        if (hello == Hello)
            std::cout << string_cast(hello) << " and " << string_cast(Hello)
                    << " are equal\n";

        if (hello == "HELLO")
            std::cout << string_cast(hello) << " and "
                    << "HELLO"
                    << " are equal\n";
    }
    else
    {
        std::cout << "wchar_t\n";
        ci_wstring hello = L"hello";
        ci_wstring Hello = L"Hello";

        // convert a ci_wstring to a std::wstring
        std::wstring x = string_cast(hello);

        // convert a std::wstring to a ci_wstring
        auto ci_hello = ci_string_cast(x);

        if (hello == Hello)
            std::wcout << string_cast(hello) << L" and " << string_cast(Hello) << L" are equal\n";

        if (hello == L"HELLO")
            std::wcout << string_cast(hello) << L" and " << L"HELLO" << L" are equal\n";
    }
}

你可以在这里玩它: https ://godbolt.org/z/5ec5sz

于 2021-03-06T17:48:16.510 回答