Reputation: 13876
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
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
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