Let's say we're given a template instantiation Container<U, Args...>
(think Container
being an std::vector
) and a non-template type T, and we need to check if we can call push_back
on an object of type Container<T>
. Here's the code that uses the detector idiom:
#include <iostream>
#include <vector>
#include <set>
#include <string>
#include <type_traits>
#include <boost/iterator.hpp>
#include <boost/range.hpp>
template<typename, typename>
struct replace
{
using type = struct Error;
};
template<template<typename...> class Container, typename U, typename T>
struct replace<Container<U>, T>
{
using type = Container<T>;
};
template<typename Container, typename T>
using replace_t = typename replace<Container, T>::type;
template<typename Placeholder, template<typename...> class Op, typename... Args>
struct isDetected : std::false_type {};
template<template<typename...> class Op, typename... Args>
struct isDetected<std::void_t<Op<Args...>>, Op, Args...> : std::true_type {};
template<typename Container, typename T>
using pushBackDetector = decltype(std::declval<Container&>().push_back(std::declval<T>()));
template<typename Container, typename T>
bool canPushBack()
{
return isDetected<void, pushBackDetector, Container, T> {};
}
int main()
{
std::cout << canPushBack<replace_t<std::vector<int>, double>, double>() << std::endl;
std::cout << canPushBack<replace_t<std::set<int>, double>, double>() << std::endl;
std::cout << canPushBack<replace_t<boost::iterator_range<std::string::iterator>, std::string::iterator>, double>() << std::endl;
//std::cout << canPushBack<replace_t<boost::iterator_range<std::string::iterator>, int>, double>() << std::endl;
}
A live example is available at Wandbox.
Indeed, it correctly deduces that we can call push_back
on std::vector<double>
, but we can't do that on std::set<double>
or on boost::iterator_range<std::string::iterator>
.
Now let's check if we can call push_back
on boost::iterator_range<int>
and uncomment the last line! And now the code explodes so beautifully that I won't give the full error message here (better to do that on the live example linked above), but the gist of it is the compiler trying to instantiate boost::iterator_range<int>
and turning the failure to instantiate some base type of that one into a hard error:
/opt/wandbox/boost-1.65.1/clang-5.0.0/include/boost/iterator/iterator_categories.hpp:119:60: error: no type named 'iterator_category' in 'std::__1::iterator_traits<int>'
typename boost::detail::iterator_traits<Iterator>::iterator_category
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
/opt/wandbox/boost-1.65.1/clang-5.0.0/include/boost/range/iterator_range_core.hpp:156:32: note: in instantiation of template class 'boost::iterators::iterator_traversal<int>' requested here
BOOST_DEDUCED_TYPENAME iterator_traversal<IteratorT>::type
^
/opt/wandbox/boost-1.65.1/clang-5.0.0/include/boost/range/iterator_range_core.hpp:436:67: note: in instantiation of template class 'boost::iterator_range_detail::pure_iterator_traversal<int>' requested here
BOOST_DEDUCED_TYPENAME iterator_range_detail::pure_iterator_traversal<IteratorT>::type
^
prog.cc:31:61: note: in instantiation of template class 'boost::iterator_range<int>' requested here
using pushBackDetector = decltype(std::declval<Container&>().push_back(std::declval<T>()));
^
prog.cc:28:31: note: in instantiation of template type alias 'pushBackDetector' requested here
struct isDetected<std::void_t<Op<Args...>>, Op, Args...> : std::true_type {};
^
prog.cc:36:12: note: during template argument deduction for class template partial specialization 'isDetected<std::void_t<Op<Args...> >, Op, Args...>' [with Op = pushBackDetector, Args = <boost::iterator_range<int>, double>]
return isDetected<void, pushBackDetector, Container, T> {};
^
On one hand, this makes perfect sense — indeed, int
is not an iterator. On the other hand, it's very desirable to catch this incorrect instantiation and just return false
from canPushBack()
in this case. So, this is the question: is it possible to turn this hard error into a soft error and handle it gracefully?
No, you cannot take a template that doesn't support SFINAE-detection and make it SFINAE-friendly without manual work specific to the type in question, and sometimes that isn't enough.
The best you can do is write a manual trait that does it for you, and an alias that SFINAE checks if it can be applied, and only returns a type if it can be.
What more, it isn't possible to detect if something is an iterator. There are no standard-mandated SFINAE-friendly "is X an iterator" tests. As a general rule, all iterators must support std::iterator_traits<T>
, but there are zero requirements that non-iterators must generate a SFINAE friendly result when you pass them to std::iterator_traits
, and in my experience passing void*
to std::iterator_traits
generates a non-SFINAE friendly result.
You could try to hack one -- detect the various things that an iterator must do (be dereferenceable, incrementable, equally compariable), but even there a type may not have SFINAE-friendly errors when you try it. For example, take non-equally compariable type and put it in a std::vector
, and attempting to do ==
can fail to compile with a hard error (at least the last time I checked).
A simple case is:
template<class T>
struct problem {
static_assert(!std::is_same<T,int>{}, "oh oh");
};
passing int
to problem
cannot be SFINAE detected as being a problem. If you instantiate problem<int>
, you get a hard error.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With