6

Related but much more arcane than C++11 static assert for equality comparable type?

JF Bastien's paper N4130 "Pad Thy Atomics!" got me thinking that if we're going to use atomic<T>::compare_exchange_weak() where T is a class or struct type, such as

struct Count {
    int strong_count;
    int weak_count;
};

then we really want to static-assert two things:

First, that T is actually lock-free atomic:

template<class T>
static constexpr bool is_lockfree_atomic_v =
    std::atomic<T>::is_always_lock_free;

And second, that compare_exchange_weak will do what we want. Recall (or from N4130) that compare_exchange_weak is defined in terms of memcmp and memcpy. So we need to check that those functions will do the right thing as far as T is concerned:

template<class T>
static constexpr bool is_cmpxchgable_v =
    std::is_trivially_copyable_v<T> &&
    is_trivially_equality_comparable_v<T>;

is_trivially_copyable_v is provided by the STL. But we don't have an is_trivially_equality_comparable_v yet — and my understanding, sadly, is that P0515 Consistent Comparison does not propose to provide one. (P0515 is the feature that will allow the compiler to detect that the equality operator is literally "trivial" — that it is not user-provided and that it is explicitly defaulted under such-and-such conditions. However, it does not introduce any new concept such as "trivially comparable" into the core language.)

My best stab at a "trivially comparable" trait looks like this:

template<class T, class U>
static constexpr bool is_equality_comparable_with_v =
    requires(std::declval<T>() == std::declval<U>());

template<class T>
static constexpr bool is_equality_comparable_v =
    is_equality_comparable_with_v<T, U>;

template<class T>
static constexpr bool is_trivially_equality_comparable_v =
    is_equality_comparable_v<T> &&
    std::has_unique_object_representations_v<T>;

However, this definition relies on std:: has_unique_object_representations_v, [EDIT: which has undefined behavior whose value is unrelated to the behavior of operator==] when T is not a scalar type. And I strongly suspect that in practice has_unique_object_representations_v will return garbage for struct types such as my original Count type.

struct Yes { int a; int b; };
struct No { short a; int b; };
using Yes2 = std::tuple<int, int>;
struct No2 { int a; int b; bool operator==(const No2&) { return true; }};
  • Clang/libc++ does not implement has_unique_object_representations yet.
  • MSVC does not implement has_unique_object_representations yet, AFAIK.
  • GCC/libstdc++ says that has_unique_object_representations_v<Yes>, and even reports correctly that not has_unique_object_representations_v<No>, but reports incorrectly that not has_unique_object_representations_v<Yes2> and that has_unique_object_representations_v<No2>.

So my questions are, (1) is there a better way to test for "trivial comparability"? and (2) is there any movement in the direction of a proposal for is_trivially_equality_comparable (and is_trivially_less_than_comparable and so on), if P0515 gets into the standard for C++20? and (3) should there be?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
  • 2
    In what universe does `No2` not have unique object representations? – T.C. Dec 10 '17 at 18:37
  • Then test for pod before testing for unique representation? – Passer By Dec 10 '17 at 18:47
  • 1
    @T.C.: `No2` has many different possible bit-level object representations, but all of them compare equal. So in `No2`'s case, `a == b` does *not* imply that `memcmp(a, b, sizeof a) == 0`. PODness is unrelated as far as I can tell. And yes, `has_unique_object_representations` is well-defined for an implementation-defined set of scalar types... ah, never mind, its behavior for class types is defined in bullet point (9.2) right above where I was looking. I'll edit the question. – Quuxplusone Dec 10 '17 at 18:54
  • 1
    As to tuple, it isn’t required to propagate copy/move triviality. – T.C. Dec 10 '17 at 18:59
  • 1
    MSVC has had `has_unique_object_representations` (There's a tongue twister for ya) IIRC since VS 15.3. The trait agrees with libstdc++ for your types `Yes`, `No`, `Yes2`, and `No2`. – Casey Dec 13 '17 at 16:10
  • @Casey: Just to confirm, you're saying MSVC agrees with GCC's answers, right? Not that MSVC reports the "correct" answer for `Yes2` and `No2`? (Last night I started working on my C++Now 2018 talk; I've got code and benchmarks for `is_trivially_comparable_v` in https://github.com/Quuxplusone/from-scratch/tree/master/cppnow2018 .) – Quuxplusone Dec 13 '17 at 18:19
  • 1
    Yes, msvc agrees with GCC's answers. – Casey Dec 13 '17 at 19:52

2 Answers2

5

I think you want has_padding_bits. p0528 explains why in details. In short, you're exactly right on memcpy and memcmp!

There will be an updated paper in the pre-Jacksonville meeting.

Update mid-2018

The standards committee decided to go in another direction with p0528, and instead make cmpxchg of types with padding bits Just Work. There won't be a has_padding_bits for the problem p0528 wanted to solve.

There's no good answer to your question at the moment :-)

JF Bastien
  • 6,673
  • 2
  • 26
  • 34
  • Hi JF! This is the first time I've more-than-skimmed P0528. After reading it, I still strongly believe that talking in terms of "padding bits" is too low-level to be useful in practice. Consider that `std::string` might not have any padding bits, yet it is not "trivially comparable". Consider that `using X = std::tuple` might not have any padding bits (because reordering of fields), and is even *trivially copyable* with `memcpy`; and is even *trivially equality comparable* with `memcmp`; but yet `X` is not *trivially less-than comparable* with `memcmp` (because reordering). – Quuxplusone Dec 11 '17 at 17:31
  • 1
    And yeah, I recognize that `std::string` isn't *trivially copyable* either, so it's not a very illuminating example. My point is: There exist types that are trivially equality-comparable yet not trivially copyable (such as `unique_ptr` [EDIT:ish]); and there exist types that are trivially copyable yet not trivially equality-comparable (such as `float`); and I really hope that C++ post-[P0515](http://wg21.link/p0515) can deal with this subject sensibly. – Quuxplusone Dec 11 '17 at 17:34
0

As of 2023, there is a patch available to add the __is_trivially_equality_comparable(T) intrinsic to Clang (motivated by users within libc++ such as std::equal and std::find); see reviews.llvm.org/D147175. The builtin reports true for types that the compiler can identify as trivially equality comparable, which (as usual) is a subset of the set of types that we'd call "Platonically" trivially equality comparable. For example:

struct A {
    int i;
    bool operator==(const A& a) const = default;
};
struct B {
    int i;
    bool operator==(const B& b) const { return i == b.i; }
};
static_assert( __is_trivially_equality_comparable(A));
static_assert(!__is_trivially_equality_comparable(B));

Here B can, physically, be compared with memcmp, but the compiler doesn't know that, because it can't see inside the curly braces. Analogous things happen with "trivially copyable," "trivially default-constructible," and so on.

There are several corner cases that are problematic for the builtin.

First of all, note the surprising false answers for enum types and empty class types.

enum E { RED, YELLOW };
struct S { bool operator==(const S&) const = default; };
static_assert(!__is_trivially_equality_comparable(E));
static_assert(!__is_trivially_equality_comparable(S));

An empty struct type isn't trivially comparable because it has padding.

The enum type E is trivially equality comparable now, but it stops being trivially comparable if someone adds a free function bool operator==(E, E) { return false; } (which is a better match than the built-in operator candidate). The proposed Clang implementation doesn't bother to check whether that happened or not, because checking is difficult; instead it considers enum types never-trivially-comparable.

In fact, the proposed Clang builtin doesn't care about non-member operators at all — not even hidden friends. It's not that it's impossible or unimplementable for it to care about non-member operators (some time in the future); it's just not implemented yet. So the proposed compiler intrinsic sees C below as non-trivially comparable:

struct C {
    int i;
    friend bool operator==(C, C) = default;
};
static_assert(!__is_trivially_equality_comparable(C));

Just as a type can be "trivially copyable" without being copyable at all, a type can be "trivially equality-comparable" without being equality-comparable at all. The compiler reports D as trivially equality-comparable, but std::equality_comparable<D> is false because d == d is ill-formed (ambiguous). This is perceived as a feature, not a bug.

struct D {
    int i;
    bool operator==(const D&) const = default;
    friend bool operator==(D, D);
};
static_assert(__is_trivially_equality_comparable(D));
static_assert(!std::equality_comparable(D));

A type can also be trivially equality comparable when const-qualified and non-trivially comparable otherwise:

struct E {
  int i;
  bool operator==(const E&) const = default;
  friend bool operator==(E&, E&) { return false; }
};

libstdc++ and libc++ disagree as to whether an algorithm like std::equal uses "const" or "non-const" comparisons when applied to a non-const range, so their std::equals give two different answers in this Godbolt. I suspect that an evil type like E produces "library UB," thus both results are conforming; but I didn't look it up.

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159