Reputation: 1861
I created a class template. Depending on its template arguments it supports different operations. The class template shall be (implicitly) instantiated for several combinations of template arguments. For some of those combinations there may be member functions that do not make sense (and also do not compile). However, as long as I do not enforce explicit instantiation, everything seems to work as desired.
Now, there are these scary cases called "unspecified", "undefined", "ill-formed; no diagnostic required", …. I definitely want to avoid any of those things. So I am asking for an advice how to deal with this situation.
Here is an example that shows the same observation. Please note that I am not so much interested in how to fix this exact toy example.
#include <iostream>
#include <type_traits>
template<class T>
struct SingleSink {
SingleSink(T) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
template<class T>
struct DoubleSink {
DoubleSink(T, T) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
template<class T, int arity /*, some other stuff */>
struct SuperSink {
// This class shall do something special depending on, say, `arity`.
// Instead of partially specializing the whole class template (and introducing
// code duplication for the remaining functionality), let us externalize the
// `arity`-dependent behavior to a special member.
using Sink = std::conditional_t<
arity == 1,
SingleSink<T>,
DoubleSink<T>
>;
Sink sink_;
// [some more data members that do not depend on `arity`]
// for a fixed `Sink` one of the following constructors should fail to compile
SuperSink(T i) : sink_{i} {}
SuperSink(T i, T j) : sink_{i, j} {}
// ... so these are what I call "conditionally invalid member functions".
};
// explicit instantiation yields error (deactivated by comments):
// template struct SuperSink<int, 1>;
// template struct SuperSink<int, 2>;
int main() {
// implicit instantiation works
SuperSink<int, 1>{5};
SuperSink<int, 2>{5, 6};
// these yield a compile error (as desired)
// SuperSink<int, 1>{5, 6};
// SuperSink<int, 2>{5};
}
Upvotes: 1
Views: 121
Reputation: 217478
Are these conditionally invalid member functions a problem if I never need explicit instantiation?
Even templates from STL have "invalid" methods, for example: std::vector<T>::resize(std::size_t)
with non default constructible T
.
So, with "invalid" methods, your class is usable normally. Document your requirement is an option.
But then, those methods are not SFINAE friendly, as error would not appear in immediate context, but in instantiation.
You might use SFINAE on your own to remove them when invalid, for example:
template <std::size_t N = arity, std::enable_if_t<N == 1, int> = 0>
SuperSink(T i) : sink_{i} {}
template <std::size_t N = arity, std::enable_if_t<N != 1, int> = 0>
SuperSink(T i, T j) : sink_{i, j} {}
In C++20, you might specify some conditions to include methods in the class (Similar to above SFINAE, but with a nicer syntax and no extra template):
SuperSink(T i) requires (arity == 1) : sink_{i} {}
SuperSink(T i, T j) requires (arity != 1) : sink_{i, j} {}
Upvotes: 3
Reputation: 4655
I see at least the problem, that the design breaks type traits like the following, so you will probably get problems when SFINAE is used to decide which constructors will be called.
static_assert(!std::is_constructible<SuperSink<int, 1>, int, int>::value);
static_assert(!std::is_constructible<SuperSink<int, 2>, int>::value);
You could fix this by inverting the design: Define SingleSink
and DoubleSink
as special cases (or if needed: specialisations) of a generic SuperSink
.
#include <iostream>
#include <type_traits>
template<class T, int arity>
struct SuperSink {
template<typename... Ts, typename = std::enable_if_t<sizeof...(Ts) == arity> >
SuperSink(Ts... is) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
template<typename T>
struct SingleSink : SuperSink<T, 1> {
using Base = SuperSink<T, 1>;
using Base::Base; // inherit constructor
// implement special functionality here
};
template<typename T>
struct DoubleSink : SuperSink<T, 2> {
// follow same pattern as in SingleSink.
}
int main() {
// implicit instantiation works
SuperSink<int, 1>{5};
SuperSink<int, 2>{5, 6};
// Now, these work as desired
static_assert(!std::is_constructible<SuperSink<int, 1>, int, int>::value);
static_assert(!std::is_constructible<SuperSink<int, 2>, int>::value);
// these yield a compile error (as desired)
// SuperSink<int, 1>{5, 6};
// SuperSink<int, 2>{5};
}
Upvotes: 1