Home > Mobile >  Why does a non-constexpr std::integral_constant work as a template argument?
Why does a non-constexpr std::integral_constant work as a template argument?

Time:05-15

My question is why the following code is valid C :

#include <iostream>
#include <tuple>
#include <type_traits>

std::tuple<const char *, const char *> tuple("Hello", "world");

std::integral_constant<std::size_t, 0> zero;
std::integral_constant<std::size_t, 1> one;

template<typename T> const char *
lookup(T n)
{
  // I would expect to have to write this:
  //   return std::get<decltype(n)::value>(tuple);

  // But actually this works:
  return std::get<n>(tuple);
}

int
main()
{
  std::cout << lookup(zero) << " " << lookup(one) << std::endl;
}

Of course, I'm happy to be able to program this way. Moreover, I understand that std::integral_constant has a constexpr conversion operator. However, the parameter n to lookup is not constexpr, so I'm confused as to how a non-static method on a non-constexpr object (even if the method itself is constexpr) can possibly return a compile-time constant.

Of course, we happen to know in this case that the body of the conversion operator doesn't look at the runtime value, but nothing in the type signature guarantees that. For example, the following type obviously doesn't work, even though it, too, has a constexpr conversion operator:

struct bad_const {
  const std::size_t value;
  constexpr bad_const(std::size_t v) : value(v) {}
  constexpr operator std::size_t() const noexcept { return value; }
};

bad_const badone(1);

Is there some extra property of methods that they are considered differently if they ignore the implicit this argument?

CodePudding user response:

Of course, we happen to know in this case that the body of the conversion operator doesn't look at the runtime value, but nothing in the type signature guarantees that.

Correct. What you are missing is the fact that this doesn't matter.

When a function is called during constant expression evaluation, the compiler checks whether the function is constexpr. If it's not constexpr, then the enclosing evaluation fails to yield a constant expression.

But if it is constexpr, then the compiler executes its body at compile time and checks whether anything in its body disqualifies the enclosing evaluation from being a constant expression.

So in the case of the conversion operator of std::integral_constant, the compiler, executing its body at compile time, sees that it simply returns the value of the template parameter, which is fine.

If the compiler were evaluating a constexpr member function of some other type, which reads a non-const non-static data member of the object (and the object is not constexpr), then the compiler, at that point, would determine that the enclosing evaluation is not a constant expression.

It's a bit upsetting that when you see a function that has been declared constexpr, this tells you nothing about which evaluations of that function yield constant expressions, but that's the way it is.

CodePudding user response:

In std::get<n>() the n needs to be a size_t but you pass a std::integral_constant<...>. So the compiler checks if there is an implicit conversion from one to the other, which attempts to call std::integral_constant<...>::operator size_t(). As it happens that is exactly the operator your std::integral_constant<std::size_t, 0> zero; has. So everything works out fine type wise.

But you asked why this is a compile-time constant: The operator size_t() returns the static constexpr T value = v;. So it doesn't matter if the integral_constant itself is const or not. It has no influence on the returned constant.

  • Related