Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Turning a hard error on incorrect template instantiation in SFINAE context into a soft error

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?

like image 560
0xd34df00d Avatar asked Nov 17 '17 14:11

0xd34df00d


1 Answers

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.

like image 126
Yakk - Adam Nevraumont Avatar answered Sep 22 '22 12:09

Yakk - Adam Nevraumont