Why does numeric_limits<atomic<X>> not fail to compile?

Viewed 132

I just created a bug by testing a value against the following expression:

std::numeric_limits<decltype(allocationCount)>::max()

In this context, allocationCount is a std::atomic<std::size_t>.

Apparently, the above mentioned expression compiles and evaluates to 0 on both Clang 10 and GCC 10:

#include <atomic>
#include <cstdint>
#include <limits>
#include <string>

static std::atomic<std::size_t> allocationCount = 0;

uint64_t buggyGetMax() {
    return std::numeric_limits<decltype(allocationCount)>::max();
}

uint64_t correctGetMax() {
    return std::numeric_limits<decltype(allocationCount)::value_type>::max();
}

What I meant to use was

std::numeric_limits<decltype(allocationCount)::value_type>::max()

that produces the value I wanted, that is std::numeric_limits<std::size_t>::max().


The question I have is why std::numeric_limits<decltype(allocationCount)> even compiled? Shouldn't it fail as std::numeric_limits<std::string> does?

If this is by design, why is the max() 0?

2 Answers

This is the default behavior. From [numeric.limits]

  1. For all members declared static constexpr in the numeric_­limits template, specializations shall define these values in such a way that they are usable as constant expressions.
  2. The default numeric_­limits<T> template shall have all members, but with 0 or false values.
  3. Specializations shall be provided for each arithmetic type, both floating-point and integer, including bool. The member is_­specialized shall be true for all such specializations of numeric_­limits.
  4. The value of each member of a specialization of numeric_­limits on a cv-qualified type cv T shall be equal to the value of the corresponding member of the specialization on the unqualified type T.
  5. Non-arithmetic standard types, such as complex<T>, shall not have specializations.

So, since std::atomic<T> is a non-arithmetic type, it shall not have a specialization per paragraph 6 and that means paragraph 3 comes into play and all values you get will be 0 or false.

std::numeric_limits<T> has a default implementation when it does not have a specialization for T.

So, when T=std::string and T=std::atomic<size_t>, std::numeric_limits will still compile fine, it will just report default values.

However, inside of your buggyGetMax() function, which returns a uint64_t, returning std::numeric_limits<std::string>::max() will not compile because max() returns an empty std::string which does not implicitly convert to uint64_t. But std::atomic<size_t> does implicitly convert to uint64_t, which is why returning std::numeric_limits<std::atomic<std::size_t>>::max() compiles. It just won't return the value you are expecting.

Related