C++ template Concept that requires specific parameter pack size

Viewed 1582

Edit: This funcion have to check types one by one and return obj of any that will satisfy condition or nullptr.

template <typename... Args, typename = std::enable_if_t<(sizeof...(Args) == 0)>()>
std::nullptr_t f() { return nullptr; }

template <typename T, typename... Args>
BaseClassOfAllArgs* f() {
    if (<some condition related to T...>)
        return new T;
    return f<Args...>();
}

This code works for me. But I wonder if is it possible to rewrite this code to use concept? I mean something like this:

template <typename... Args>
concept NoArgs = (sizeof...(Args) == 0);

and then use it instead of std::enable_if(this code doesnt work)

template <NoArgs Args>
std::nullptr_t f() { return nullptr; }

template <typename T, typename... Args>
BaseClassOfAllArgs* f() {
    if (<some condition related to T...>)
        return new T;
    return f<Args...>();
}

EDIT: Here is working example of code after taking some tips from guys in comments. After I added 'Base' to the template it turns out that EmptyPack concept is no longer needed. And First template naturally needs 3 typenames. However, I am not sure about this concept EmptyPack. Is it really making my program ill-formed no diagnostic required?

#include <iostream>
#include <type_traits>
#include <typeinfo>
class X {};

class A : public X {};
class B : public X {};
class C : public X {};
class D : public C {};
class E {};

template<class T, class... Args >
concept DerivedsOfBase = (std::is_base_of_v<T, Args> && ...);

template<typename... Args>
concept EmptyPack = sizeof...(Args) == 0;

template<typename T>
std::nullptr_t f() {
    std::cout << "End of the types list" << std::endl;
    return nullptr;
}

template<typename Base, typename T, typename... Args> requires DerivedsOfBase<Base, T, Args...>
Base* f() {
    std::cout << typeid(T).name() << std::endl;
    if (<some condition related to T>)
        return new T;
    return f<Base, Args...>();
}

int main()
{
    auto ptr = f<X, A, B, C>();
    auto ptr2 = f<X, A, B, D>();
    //auto ptr3 = f<X, A, B, E>(); // compile error
    return 0;
}
2 Answers

Any template parameter pack that only has valid instantiations for packs of size 0 makes your program ill-formed, no diagnostic required.

This applies to your first "working" example, as well as any practical variant I can think of using concepts.

From the N3690 draft standard [temp.res] 14.6/8:

If every valid specialization of a variadic template requires an empty template parameter pack, the template is ill-formed, no diagnostic required.

(I've seen that in many versions of C++, I'm just using a random draft standard as it showed up when I googled C++ standard pdf.)

Note that (a) your two f are overloads not specializations, and (b) what C++ programmers mean by "valid specialization of a template" and what the standard means are not quite the same thing.

In essence, pretty much any attempt at your concept will result in ill-formed, no diagnostic required programs.

To see the problem here, we'll reword the standard using negation:

If (every valid specialization of a variadic template requires an empty template parameter pack) then (the template is ill-formed, no diagnostic required).

We can convert this "forall" into a "there exists". "Forall X, P(X)" is the same as "Not( There exists X, Not P(X) )".

Unless (there exists a valid specialization of a variadic template with a non-empty template parameter pack) then (the template is ill-formed, no diagnostic required).

If a variadic template has a requires clause that mandates the variadic template pack is empty, then no valid specialization of that template with an empty parameter pack exists. So your template is ill-formed, no diagnostic required.


In general, these kind of rules exist so that compilers can check if your code would always be nonsense. Things like templates where no type parameter could make it compile, or packs that must be empty, are generally a sign that your code has bugs. By making it ill-formed and not requiring a diagnostic, compilers are permitted to emit diagnostics and fail to compile.

An issue I have with the standard is that it is not just permitted to fail to compile, but it is permitted to compile a program that does literally anything.

Because the compiler is permitted to do this, and some optimizations in other cases actually result in this happening, you should avoid writing code that is ill-formed, no diagnostic required, like the plague.


A workaround is:

namespace implementation_details {
  struct never_use_me;
}
template <class=implementation_details::never_use_me>
std::nullptr_t f() { return nullptr; }

template <typename T, typename... Args>
T* f() {
  if (<some condition related to T...>)
    return new T;
  return f<Args...>();
}

another option is:

template <typename T, typename... Args>
T* f() {
  if (<some condition related to T...>)
    return new T;
  if constexpr (sizeof...(Args)==0)
    return nullptr;
  else
    return f<Args...>();
}

You don't really need concepts for that, simply use if constexpr:

T* f() {
    if constexpr (sizeof...(Args) != 0) {
        if (<some condition related to T...>)
            return new T;
        return f<Args...>();
    } else {
        return nullptr;
    }
}

If you really want separated functions, you could always use overloading:

template <typename T> // no pack
T* f() {
    return nullptr;
}

template <typename T, typename Arg1, typename... Args> // one of more Args
T* f() {
    if (<some condition related to T...>)
        return new T;
    return f<Arg1, Args...>();
}

But of course you can always use requires expressions if you really want to keep the same logic you had with SFINAE. It should be possible (and enough) without a declared concept, but only with constraint:

template <typename T, typename...> // pack logically always empty
T* f() {
    return nullptr;
}

template <typename T, typename... Args> requires (sizeof...(Args) > 0)
T* f() {
    if (<some condition related to T...>)
        return new T;
    return f<Args...>();
}

Of course you can also wrap the condition inside a concept, but you gain very little using this technique in this case, since you still have to use the concept inside a require clause:

template<typename... Args>
concept nonempty_pack = sizeof...(Args) > 0;

template <typename T, typename...> // pack logically always empty
T* f() {
    return nullptr;
}

template <typename T, typename... Args> requires nonempty_pack<Args...>
T* f() {
    if (<some condition related to T...>)
        return new T;
    return f<Args...>();
}

The syntax template<my_concept T> will send T as the first parameter. You'll always get that parameter snet automatically, hence the need to put the concept in the requires clause.

Related