This is an unofficial snapshot of the ISO/IEC JTC1 SC22 WG21 Core Issues List revision 114a. See http://www.open-std.org/jtc1/sc22/wg21/ for the official list.

2024-04-18


1581. When are constexpr member functions defined?

Section: 6.3  [basic.def.odr]     Status: CD5     Submitter: Richard Smith     Date: 2012-10-29

[Accepted as a DR as paper P0859R0 at the October, 2017 meeting.]

11.4.4 [special] is perfectly clear that special member functions are only implicitly defined when they are odr-used. This creates a problem for constant expressions in unevaluated contexts:

   struct duration {
     constexpr duration() {}
     constexpr operator int() const { return 0; }
   };
   // duration d = duration(); // #1
   int n = sizeof(short{duration(duration())});

The issue here is that we are not permitted to implicitly define constexpr duration::duration(duration&&) in this program, so the expression in the initializer list is not a constant expression (because it invokes a constexpr function which has not been defined), so the braced initializer contains a narrowing conversion, so the program is ill-formed.

If we uncomment line #1, the move constructor is implicitly defined and the program is valid. This spooky action at a distance is extremely unfortunate. Implementations diverge on this point.

There are also similar problems with implicit instantiation of constexpr functions. It is not clear which contexts require their instantiation. For example:

  template<int N> struct U {};
  int g(int);
  template<typename T> constexpr int h(T) { return T::error; }
  template<typename T> auto f(T t) -> U<g(T()) + h(T())> {}
  int f(...);
  int k = f(0);

There are at least two different ways of modeling the current rules:

These two approaches can be distinguished by code like this:

  int k = sizeof(U<0 && h(0)>);

Under the first approach, this code is valid; under the second, it is ill-formed.

A possible approach to resolving this issue would be to change the definition of “potentially-evaluated” such that template arguments, array bounds, and braced-init-lists (and any other expressions which are constant evaluated) are always potentially-evaluated, even if they appear within an unevaluated context, and to change 13.9.2 [temp.inst] paragraph 3 to say simply that function template specializations are implicitly instantiated when they are odr-used.

A related question is whether putatively constexpr constructors must be instantiated in order to determine whether their class is a literal type or not. See issue 1358.

Jason Merrill:

I'm concerned about unintended side-effects of such a large change to “potentially-evaluated;” I would prefer something that only affects constexpr.

It occurs to me that this is parallel to issue 1330: just like we want to instantiate exception specifiers for calls in unevaluated context, we also want to instantiate constexpr functions. I think we should define some other term to say when there's a reference to a declaration, and then say that the declaration is odr-used when that happens in potentially-evaluated context.

Notes from the April, 2013 meeting:

An additional question was raised about whether constexpr functions should be instantiated as a result of appearing within unevaluated subexpressions of constant expressions. For example:

  #include <type_traits>

  template <class T> constexpr T f(T t) { return +t; }

  struct A { };

  template <class T>
  decltype(std::is_scalar<T>::value ? T::fail : f(T()))
    g() { }

  template <class T>
  void g(...);

  int main()
  {
    g<A>();
  }

If constexpr instantiation happens during constant expression evaluation, f<A> is never instantiated and the program is well-formed. If constexpr instantiation happens during parsing, f<A> is instantiated and the program is ill-formed.