Zebrafish
Zebrafish

Reputation: 13876

Is this allowed syntax for defaulted template argument?

I want to be able to have a template type argument as empty, which case the class just has an empty T, or U, or whatever. I tried to do this defaulting the template argument to a simple lambda:

template <typename U = decltype([]() {})>

On Visual Studio 2019 16.9.3 I don't get the right result when I query the type of U, but by trying to compile on OnlineGDB I found out that this probably shouldn't be compiling anyway because the defaulted argument is defining a new type:

    #include <iostream>
    template <typename T, typename U = decltype([]() { }) >
    struct Planet
    {
        T T_obj;
        [[no_unique_address]] U U_obj;
    };
    
    int main()
    {
        Planet<int, char> planet;
    
        std::cout << "Typename = " << typeid([]() {}).name() << '\n'; // Type here is a lambda, which is correct
        std::cout << "Typename of U = " << typeid(planet.U_obj).name() << '\n'; // Type here is int, why?
        std::cout << "Typename of U = " << typeid(Planet<int>::U_obj).name() << '\n'; // Type here is int, why?
    }

Is this just some sort of bug? If I cannot do 'decltype([] () {})' then I can just define 'struct EmptyType{};' just above the template without a problem.

Upvotes: 0

Views: 89

Answers (2)

2b-t
2b-t

Reputation: 2564

The correct answer to the question was given by Brian Bi (see above). This post is just a follow-up that started from a comment discussion and was requested by the original poster to be elaborated in more detail and might be helpful to somebody trying achieve the same behaviour. Therefore I decided to post it over here rather than somewhere hidden in the comment. The solutions are not equivalent (e.g. stack vs heap allocation) and partially also have a different interface.


Dummy struct

One possible solution would be to simply pass a dummy struct as a default parameter as follows (Wandbox)

struct DummyStruct {
};
template <typename T, typename U = DummyStruct>
struct Planet {
    Planet(T const t, U const u = {})
    : t_{t}, u_{u} {
        return;
    }
    T t_;
    U u_;
};

Pointer to void

Similarly one could use plain or smart pointers and a default type void. The given constructor will not work with template deduction but you can always write a constructor with smart pointers (Wandbox).

template <typename T, typename U = void>
struct Planet {
    // Trick to disable this constructor for type void in order to avoid compilation error
    template<typename T1 = T, typename U1 = U,
             typename std::enable_if_t<!std::is_same_v<U1,void> && std::is_same_v<U1,U>>* = nullptr>
    Planet(T1 const& t, U1 const& u)
    : t_{std::make_shared<T>(t)}, u_{std::make_shared<U>(u)} {
        return;
    }
    Planet(T const& t)
    : t_{std::make_shared<T>(t)}, u_{nullptr} {
        return;
    }
    std::shared_ptr<T> t_;
    std::shared_ptr<U> u_;
};

Variadic templates and tuples

One might as well use variadic templates in combination with tuples and then access them with std::get(...) but the usage might be a bit inconvenient (Wandbox)

template <typename... Ts>
struct Planet {
    Planet(Ts const&... t)
    : t_{std::make_tuple(t...)} {
        return;
    }
    std::tuple<Ts...> t_;
};

Polymorphic vector

Another possibility in some special cases where the involved containers are derived from a common base class and therefore share a common interface is using a std::vector of smart pointers such as (Wandbox):

struct Parent {
    virtual void update() = 0;
};

struct Child: public Parent {
    void update() override {
        // Do something
        return;
    }
};

struct Planet {
    template <typename... Ts>
    Planet(std::shared_ptr<Ts>&... t)
    : t_{t...} {
        // Static assertion with fold expression
        static_assert((std::is_base_of_v<Parent, Ts> && ...), "Template arguments must inherit from 'Parent'.");
        return;
    }
    
    std::vector<std::shared_ptr<Parent>> t_;
};

Upvotes: 1

Brian Bi
Brian Bi

Reputation: 119194

In C++17, a lambda may not appear in an unevaluated context. This restriction was lifted in C++20, but your compiler might not have implemented this feature yet (even if it already supports [[no_unique_address]], another C++20 feature), so you need to upgrade your compiler. And in any case I would recommend not writing code like this, because it would mean that Planet<int> would not be the same type as Planet<int> (even within the same TU), as a new lambda type will be created every time the default argument is used. void might work better as the default type, and you can give U_obj some private empty class type in the case where U is void.

Upvotes: 4

Related Questions