Home > other >  Templated constexpr function invocation with partially defined class changes subsequent results
Templated constexpr function invocation with partially defined class changes subsequent results

Time:05-26

I have a struct with multiple overloads of a static function taking some counter<int> as an argument:

struct S {
    static void fn(counter<1>);
    static void fn(counter<2>);
    static void fn(counter<3>);
};

A templated constexpr function can be used to look for a specific overload in that class:

template <typename T>
inline constexpr size_t count_fns() {
    // 'defines_fn' is a type trait, full code in demo link
    if constexpr (defines_fn<T, counter<99> >::value) { return  99; }
    if constexpr (defines_fn<T, counter<98> >::value) { return  98; }
    if constexpr (defines_fn<T, counter<97> >::value) { return  97; }
    // [...]
    if constexpr (defines_fn<T, counter<3> >::value) { return  3; }
    if constexpr (defines_fn<T, counter<2> >::value) { return  2; }
    if constexpr (defines_fn<T, counter<1> >::value) { return  1; }
    return 0;
}

In ordinary usage, count_fns<S>() returns 3, as expected.

However, adding an inline static variable invoking count_fns changes things:

struct U {
    static void fn(counter<1>);
    static void fn(counter<2>);
    static constexpr size_t C0 = count_fns<U>();  // C0 is 2
    static void fn(counter<3>);
};

static_assert(count_fns<U>() == 3, " <-- fails, value is actually 'still' 2");

Godbolt suggests this behavior is consistent across compilers (MSVC, gcc, clang): Demo

Edit: In addition, it should be noted that the behavior is different within the definition of a templated class (the following compiles on clang and gcc, but not MSVC):

template <typename T>
struct V {
    static void fn(counter<1>);
    static void fn(counter<2>);
    static constexpr size_t C0 = count_fns<V>();  // C0 is now 3
    static void fn(counter<3>);
};

static_assert(count_fns<V>() == 3, " <-- this is now true!");

Is this to be expected, or is it some sort of undefined behavior with the constexpr interpreter?


These are the definitions of counter and defines_fn:

template <size_t Value>
struct counter {
    static constexpr size_t value = Value;
};

template <typename T, typename Arg, class = void>
struct defines_fn { static constexpr bool value = false; };

template <typename T, typename Arg>
struct defines_fn<T, Arg, std::void_t<decltype(T::fn(std::declval<Arg>()))> >
{
    static constexpr bool value = true;
};

CodePudding user response:

The section of the standard that is supposed to govern this kind of code is [temp.point]. Unfortunately, it's considered to be defective. CWG 287

The standard technically says that in your example, where count_fns<U> is referenced inside the definition of U, the point of instantiation is considered to be right after the definition of U. However, this leads to absurdities; what should happen if, for example, we had the following declaration inside U:

static void fn(counter<2 * C0>);

Now it seems that we have a circular dependency.

CWG 287 concerned class template specializations. The issue with those is a bit different because [temp.point] says that (for example) if you were to reference a class template specialization within the definition of U then the point of instantiation would be before the definition of U. (I haven't yet figured out why the rules are different for class templates and function templates.)

To solve the issues in both cases, it seems the "common sense" approach is that both template and non-template constructs referenced from inside a class definition should be able to see previously declared members of the class (and this holds for both function and class template specializations). (If the context from which they're referenced is a complete-class context, they should be able to see all members of the class. Is that possible or does it lead to other issues? I'm not sure. So I'm going to just avoid this issue for now.)

Following that principle, if the initializer of C0 requires a template specialization, compilers seem to place the point of instantiation either right before or right after the declaration of C0. There is implementation divergence on whether it's before or after, but in any case, it's after the member declarations that precede C0, and before the member declarations that follow C0. All major compilers seem to agree on this point.

The second instantiation of count_fns<U>, in the static_assert declaration, references the same set of specializations of the defines_fn class template as the first instantiation did. Class template specializations, unlike function template specializations, are not re-instantiated every time they are referenced; the first instantiation is "cached". See [temp.point] p4 and p7. So the second call to count_fns<U> returns the same result as the first.

So that seems to be the reason why you're seeing the behaviour you're seeing. Whether it's right or wrong, we can't say, until CWG 287 is fixed.

  • Related