Is Stateful Metaprogramming Ill-Formed (Yet)

Is stateful metaprogramming ill-formed (yet)?

This is CWG active issue 2118:

Defining a friend function in a template, then referencing that function later provides a means of capturing and retrieving metaprogramming state. This technique is arcane and should be made ill-formed.

Notes from the May, 2015 meeting:

CWG agreed that such techniques should be ill-formed, although the mechanism for prohibiting them is as yet undetermined.

It's still an active issue, nothing will change in C++17 at least for now. Though when such a prohibition mechanism is determined, this may be retroactively ruled as a DR.

Behaviour of friend function template returning deduced dependent type in class template

Which compiler has the correct behaviour? Or, is the code ill-formed NDR or undefined behaviour? (And, why?)

As @Sedenion points out in a comment, whilst touching upon the domain of CWG 2118 (we'll return to this further down) this program is by the current standard well-formed and GCC is correct to accept it, whereas Clang and MSVC are incorrect to reject it, as governed by [dcl.spec.auto.general]/12 and /13 [emphasis mine]:

/12 Return type deduction for a templated entity that is a function or function template with a placeholder in its declared type
occurs when the definition is instantiated even if the function body contains a return statement with a non-type-dependent operand.

/13 Redeclarations or specializations of a function or function template
with a declared return type that uses a placeholder type shall also
use that placeholder, not a deduced type
. Similarly, redeclarations or
specializations of a function or function template with a declared
return type that does not use a placeholder type shall not use a
placeholder.

[Example 6:

auto f();
auto f() { return 42; } // return type is int
auto f(); // OK
// ...
template <typename T> struct A {
friend T frf(T);
};
auto frf(int i) { return i; } // not a friend of A<int>

Particularly the "not a friend of A<int>" example of /13 highlights that the redeclaration shall use a placeholder type (frf(T), and not a deduced type (frf(int)), whereas otherwise that particular example would be valid.

/12, along with [temp.inst]/3 (below) covers the that return type deduction for the friend occurs only after the friend's primary template definition has been made available (formally: it's declaration but not definition has been instantiated) from the instantiation of the S<double> enclosing class template specialization.

The implicit instantiation of a class template specialization causes

  • (3.1) the implicit instantiation of the declarations, but not of the definitions, of the non-deleted class member functions, member
    classes, scoped member enumerations, static data members, member
    templates, and friends; and
  • [...]

However, for the purpose of determining whether an instantiated
redeclaration is valid according to [basic.def.odr] and [class.mem], a
declaration that corresponds to a definition in the template is
considered to be a definition.

[Example 4:

// ...

template<typename T> struct Friendly {
template<typename U> friend int f(U) { return sizeof(T); }
};
Friendly<char> fc;
Friendly<float> ff; // error: produces second definition of f(U)

— end example]



CWG 2118: Stateful metaprogramming via friend injection

As covered in detail e.g. in A foliage of folly, one can rely on the single first instantiation of a class template to control how the definition of a friend looks like. First here means essentially relying on meta-programming state to specify this definition.

In OP's example the first instantiation, S<double>, is used to set the definition of the primary template of the friend foo such that its deduced type will always deduce to double, for all specializations of the friend. If we ever (in the whole program) instantiate, implicitly or explicitly, a second instantiation of S (ignoring S being specialized to remove the friend), we run into ODR-violations and undefined behavior. This means that this kind of program is, in practice, essentially useless, as it would serve clients undefined behavior on a platter, but as covered in the article linked to above it can be used for utility classes to circumvent private access rules (whilst still being entirely well-formed) or other hacky mechanisms such as stateful metaprogramming (which typically runs into or beyond the grey area of well-formed).

Will friend injections be ill-formed?

From P1787R6:

Merged [temp.inject] into [temp.friend]

(Adopted in N4885 in March 2021)

The current draft (N4901) reads ([temp.friend]p2):

Friend classes, class templates, functions, or function templates can be declared within a class template.
When a template is instantiated, its friend declarations are found by name lookup as if the specialization had
been explicitly declared at its point of instantiation

Which seems to cover the old [temp.inject]p2

Storing States in C++ Metaprogramming?

It's not possible, the TMP part of C++ is a purely functional language without the concept of side effects.

Meta programming: Declare a new struct on the fly

In C++20:

using A = decltype([]{}); // an idiom
using B = decltype([]{});
...

This is idiomatic code: that’s how one writes “give me a unique type” in C++20.

In C++11, the clearest and simplest approach uses __LINE__:

namespace {
template <int> class new_type {};
}

using A = new_type<__LINE__>; // an idiom - pretty much
using B = new_type<__LINE__>;

The anonymous namespace is the most important bit. It is a serious mistake not to put the new_type class in the anonymous namespace: the types then won't be unique anymore across translation units. All sorts of hilarity will ensue 15 minutes before you plan to ship :)

This extends to C++98:

namespace {
template <int> class new_type {};
}

typedef new_type<__LINE__> A; // an idiom - pretty much
typedef new_type<__LINE__> B;

Another approach would be to manually chain the types, and have the compiler statically validate that the chaining was done correctly, and bomb out with an error if you don’t. So it’d not be brittle (assuming the magic works out).

Something like:

namespace {
struct base_{
using discr = std::integral_type<int, 0>;
};

template <class Prev> class new_type {
[magic here]
using discr = std::integral_type<int, Prev::discr+1>;
};
}

using A = new_type<base_>;
using A2 = new_type<base_>;
using B = new_type<A>;
using C = new_type<B>;
using C2 = new_type<B>;

It takes only a small bit of magic to ensure that the lines with types A2 and C2 don’t compile. Whether that magic is possible in C++11 is another story.

Is stateful metaprogramming ill-formed (yet)?

This is CWG active issue 2118:

Defining a friend function in a template, then referencing that function later provides a means of capturing and retrieving metaprogramming state. This technique is arcane and should be made ill-formed.

Notes from the May, 2015 meeting:

CWG agreed that such techniques should be ill-formed, although the mechanism for prohibiting them is as yet undetermined.

It's still an active issue, nothing will change in C++17 at least for now. Though when such a prohibition mechanism is determined, this may be retroactively ruled as a DR.

Constexpr counter that works on GCC 8, and is not restricted to namespace scope

The body of a constexpr function template must yield the same answer for all instatiations with the same template parameters and same arguments. You need to add a level of indirection, so the calculation can happen in the default argument of a template parameter dependent on the first.

See https://gcc.godbolt.org/z/GHfKKf

namespace Meta
{
template <typename T,int I> struct Tag {};

template<typename T,int N,bool B>
struct Checker{
static constexpr int currentval() noexcept{
return N;
}
};

template<typename T,int N>
struct CheckerWrapper{
template<bool B=Flag<Tag<T,N>>::Read(),int M=Checker<T,N,B>::currentval()>
static constexpr int currentval(){
return M;
}
};

template<typename T,int N>
struct Checker<T,N,true>{
template<int M=CheckerWrapper<T,N+1>::currentval()>
static constexpr int currentval() noexcept{
return M;
}
};

template<typename T,int N,bool B=Flag<Tag<T,N>>::ReadSet()>
struct Next{
static constexpr int value() noexcept{
return N;
}
};

template <typename T> class TaggedCounter
{
public:
template <int N=CheckerWrapper<T,0>::currentval()> static constexpr int Value(){
return Next<T,N>::value();
}
};
}

Concise bidirectional static 1:1 mapping of values and types

As @yeputons said, friend-injection can help here. It's a spooky feature, and I can't say I fully understand how it works, but here it goes.

#include <iostream>
#include <type_traits>

template <typename T>
struct tag {using type = T;};

template <typename T>
struct type_to_enum_friend_tag
{
friend constexpr auto adl_type_to_enum(type_to_enum_friend_tag);
};
template <auto E>
struct enum_to_type_friend_tag
{
friend constexpr auto adl_enum_to_type(enum_to_type_friend_tag);
};

namespace impl
{
// Would've used `= delete;` here, but GCC doesn't like it.
void adl_type_to_enum() {}
void adl_enum_to_type() {}
}

template <typename T>
constexpr auto type_to_enum_helper()
{
// Make sure our ADL works even if some stray
// identifier named `adl_type_to_enum` is visible.
using impl::adl_type_to_enum;
return adl_type_to_enum(type_to_enum_friend_tag<T>{});
}
template <typename T>
inline constexpr auto type_to_enum = type_to_enum_helper<T>();

template <auto E>
constexpr auto enum_to_type_helper()
{
// Make sure our ADL works even if some stray
// identifier named `adl_type_to_enum` is visible.
using impl::adl_enum_to_type;
return adl_enum_to_type(enum_to_type_friend_tag<E>{});
}
template <auto E>
using enum_to_type = typename decltype(enum_to_type_helper<E>())::type;


template <typename T, auto E>
struct foo
{
friend constexpr auto adl_type_to_enum(type_to_enum_friend_tag<T>)
{
return E;
}
friend constexpr auto adl_enum_to_type(enum_to_type_friend_tag<E>)
{
return tag<T>{};
}
};

enum class foo_type {bar = 42};
struct bar : foo<bar, foo_type::bar>
{
void say() {std::cout << "I'm bar!\n";}
};

int main()
{
std::cout << int(type_to_enum<bar>) << '\n'; // 42
enum_to_type<foo_type::bar>{}.say(); // I'm bar!
}

Run on gcc.godbolt.org

It appears to work on both GCC, Clang, and MSVC.

I'm using an auto template parameter, so you can map different types to constants from different enums, or even to plain integers. Constraining this to accept only a single specific enum should be easy, and is left as an exercise to the reader.


Of course, for the type-to-enum mapping you could simply add a static constexpr member variable to foo. But I don't know any good alternatives to friend-injection for the enum-to-type mapping.



Related Topics



Leave a reply



Submit