40

Consider this code:

#include <functional>
#include <typeinfo>

template <typename T>
inline constexpr const void *foo = &typeid(T);

int main()
{
    constexpr bool a = std::less<const void*>{}(foo<int>, foo<float>);
} 

Run on gcc.gotbolt.org

If I use < instead of std::less here, the code doesn't compile. This is not surprising, because the result of a relational pointer comparsion is unspecified if the pointers point to unrelated objects, and apparently such a comparsion can't be done at compile-time.

<source>:9:20: error: constexpr variable 'a' must be initialized by a constant expression
    constexpr bool a = foo<int> < foo<float>;
                   ^   ~~~~~~~~~~~~~~~~~~~~~
<source>:9:33: note: comparison has unspecified value
    constexpr bool a = foo<int> < foo<float>;
                                ^

The code still doesn't compile, even if I use std::less. The compiler error is the same. std::less appears to be implemented as < in at least libstdc++ and libc++; I get the same results on GCC, Clang, and MSVC.

However, the cppreference page about std::less claims that:

  1. Its operator() is constexpr.

  2. It magically implements strict total order on pointers, i.e. can be used to compare unrelated pointers with sensible results.

So, is it a bug in all those compilers, or am I missing some detail about std::less that makes the code above ill-formed?

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Off-topic: Converting both pointers to uintptr_t before comparison doesn't appear *'magical'* to me... – Aconcagua Jun 20 '20 at 21:49
  • @Aconcagua If it does that, then there is no way for it to be `constexpr`. – Brian Bi Jun 20 '20 at 21:53
  • Found [another relevant paragraph in cppreference](https://en.cppreference.com/w/cpp/language/pointer#Pointers_to_objects) (at the end of the section). – Nelfeal Jun 20 '20 at 22:07
  • If you don't use `a`, it can simply be optimised away. What happens if you use it? For instance, with `return a ? 99 : 101 ;` – TonyK Jun 21 '20 at 17:23
  • @TonyK The code doesn't compile even if the variable is not used, so I doubt it would compile if I used it somehow. – HolyBlackCat Jun 21 '20 at 18:23
  • Oh yes, you are right. I got as far as "If I use `<` instead of `std::less` here, the code doesn't compile" and assumed that if you use `std::less` the code does compile. – TonyK Jun 21 '20 at 19:38
  • Actually MSVC compiles your example. But `std::less` output is not useful: https://gcc.godbolt.org/z/b54xzn4v7 – Fedor Sep 26 '21 at 10:27

3 Answers3

27

I don't think there's a clear answer to the question that you're asking. This is a specific case of LWG 2833: marking a library function constexpr does not explain the circumstances under which calling the function will yield a constant expression.

Until this issue is resolved, I think you simply cannot rely on std::less being able to compare unrelated pointers at compile time.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
7

To be valid constexpr function, it should have parameters for which the result is constexpr, not necessary all parameters.

For example

constexpr int foo(bool b) { if (!b) throw 42; return 42; }

is valid, f(true) can be used in constexpr (even if f(false) cannot).

constexpr int a[2]{};
constexpr bool b = std::less<const void*>{}(&a[0], &a[1]);

is valid and is enough to allow less::operator() to be constexpr.

I don't think it is specified which ranges/values are correct for constexpr in standard.

So all compilers are correct.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
0

In your question, you declare a variable like constexpr bool a = std::less<const void*>{}(foo<int>, foo<float>);, a constexpr variable is required to satisfy the following rule:

A constexpr specifier used in an object declaration declares the object as const. Such an object shall have literal type and shall be initialized. In any constexpr variable declaration, the full-expression of the initialization shall be a constant expression. (Note the emphasized part)

a constant expression must be a core constant expression, hence it must satisfy the core constant expression rules:

a relational or equality operator where the result is unspecified;

In your code, &typeid(int) and &typeid(float) is unspecified due to:

Comparing unequal pointers to objects is defined as follows:

  • If two pointers point to different elements of the same array, or to subobjects thereof, the pointer to the element with the higher subscript compares greater.
  • If two pointers point to different non-static data members of the same object, or to subobjects of such members, recursively, the pointer to the later declared member compares greater provided the two members have the same access control and provided their class is not a union.
  • Otherwise, neither pointer compares greater than the other.

&typeid(int) and &typeid(float) conform with the third bullet point. And

If two operands p and q compare equal, p<=q and p>=q both yield true and pq both yield false. Otherwise, if a pointer p compares greater than a pointer q, p>=q, p>q, q<=p, and q<p all yield true and p<=q, p<q, q>=p, and q>p all yield false. Otherwise, the result of each of the operators is unspecified.

So, the result of comparing &typeid(int) with &typeid(float) is unspecified, Hence it's not a constant expression.

xmh0511
  • 7,010
  • 1
  • 9
  • 36
  • @HolyBlackCat Modfied my answers. – xmh0511 Jun 22 '20 at 12:35
  • Yep, now it looks like a correct explanation of why `<` doesn't work at compile-time in this case. But still, it's unclear if `less` should've been implemented using some compiler magic instead of `<` to make it work at compile-time even for unrelated pointers. (According to Brian's answer, the standard doesn't specify if it should or not.) – HolyBlackCat Jun 22 '20 at 12:52
  • @HolyBlackCat As far as I know, it can't because the entire `typeid` is implementation-defined and if the operand is of polymorphic class type, it would be a runtime value. – xmh0511 Jun 22 '20 at 13:04