Search code examples
c++templatesboostc++14if-constexpr

Convert if constexpr based C++17 templatized code to C++14


I am working on downgrading a project written in C++ 17 to C++ 14. While downgrading, I came across a piece of code involving if constexpr and I wish to convert it to C++ 14 (From what I know, if constexpr is a C++ 17 feature).

Boost's is_detected is used to check if a given type has star operator or get method.

#include <iostream>
#include <boost/type_traits/is_detected.hpp>
#include <type_traits>
#include <boost/optional/optional.hpp>
#include <memory>
#include <typeinfo>

template < template < typename... > typename Operation, typename... Args >
constexpr bool is_detected_v = boost::is_detected< Operation, Args... >::value;

template < typename T >
using has_star_operator = decltype( *std::declval< T >( ) );

template < typename T >
using has_get_method = decltype( std::declval< T >( ).get( ) );

There is a function call deref which is used to dereference types like pointers, arrays, iterators, smart pointers, etc.

template < typename T >
inline constexpr const auto&
deref( const T& value )
{
    if constexpr ( is_detected_v< has_star_operator, T > )
    {
        return deref( *value );
    }
    else if constexpr ( is_detected_v< has_get_method, T > )
    {
        return deref( value.get( ) );
    }
    else
    {
        return value;
    }
}

I tried to form a solution without if constexpr by using std::enable_if as below:

template <typename T>
typename std::enable_if<
    !is_detected_v<has_get_method, T> && is_detected_v<has_star_operator, T>,
    decltype( *std::declval< const T >( ) )>::type
deref(const T& value)
{
    std::cout << "STAR " << typeid(*value).name() << std::endl;
    return *value;
}

template <typename T>
typename std::enable_if<
    is_detected_v<has_get_method, T>, 
    decltype( std::declval< const T >( ).get( ) ) >::type
deref(const T& value)
{
    std::cout << "GET " << typeid(value.get()).name() << std::endl;
    return value.get();
}

template <typename T>
typename std::enable_if<
    !is_detected_v<has_get_method, T> && !is_detected_v<has_star_operator, T>,
    const T>::type
deref(const T& value)
{
    std::cout << "NONE\n";
    return value;
}

int main()
{
    int VALUE = 42;
    boost::optional<int> optional_value = boost::make_optional(VALUE);
    int a = 42;
    int *b = &a;
    const int array[ 4 ] = {VALUE, 0, 0, 0};
    //const auto list = {std::make_unique< int >( VALUE ), std::make_unique< int >( 0 ),
    //                   std::make_unique< int >( 0 )};
    //const auto iterator = list.begin( );
    //std::unique_ptr<int> u = std::make_unique< int >( VALUE );
    std::cout << deref(a) << std::endl;
    std::cout << deref(optional_value) << std::endl;
    std::cout << deref(b) << std::endl;
    std::cout << deref(array) << std::endl;
    //std::cout << deref(iterator) << std::endl;
    //std::cout << deref(u) << std::endl;
}

But, the above fails for cases like iterators and smart pointers where multiple dereference has to be made. For example, for a std::unique_ptr, first p.get() will be called (auto q = p.get()) followed by star operator (*q).

I am a beginner with templates and require some help in this. Please let me know how this can be solved.

I am using GCC 5.4 to compile.


Solution

  • How about a solution exploiting tag dispatch?

    The idea is to move the code from your branches to three auxiliary functions. These functions are overloaded on the last parameter, whose only purpose is to allow you calling the right one later on:

    template <typename T>
    constexpr const auto& deref(const T& value);
    
    template <typename T>
    constexpr const auto& deref(const T& value, std::integral_constant<int, 0>) {
        return deref(*value);
    }
    
    template <typename T>
    constexpr const auto& deref(const T& value, std::integral_constant<int, 1>) {
        return deref(value.get());
    }
    
    template <typename T>
    constexpr const auto& deref(const T& value, std::integral_constant<int, 2>) {
        return value;
    }
    
    template <typename T>
    constexpr const auto& deref(const T& value) {
        using dispatch_t = std::integral_constant<
            int, is_detected_v<has_star_operator, T>
                     ? 0
                     : (is_detected_v<has_get_method, T> ? 1 : 2)>;
        return deref(value, dispatch_t{});
    }
    

    With the above implementation, the following compiles:

    int main() {
        int VALUE = 42;
        boost::optional<int> optional_value = boost::make_optional(VALUE);
        int a = 42;
        int* b = &a;
        const int array[4] = {VALUE, 0, 0, 0};
        const auto list = {std::make_unique<int>(VALUE),
                           std::make_unique<int>(0), std::make_unique<int>(0)};
        const auto iterator = list.begin();
        std::unique_ptr<int> u = std::make_unique<int>(VALUE);
        std::cout << deref(a) << std::endl;
        std::cout << deref(optional_value) << std::endl;
        std::cout << deref(b) << std::endl;
        std::cout << deref(array) << std::endl;
        std::cout << deref(iterator) << std::endl;
        std::cout << deref(u) << std::endl;
    }
    

    and outputs:

    42
    42
    42
    42
    42
    42
    

    Also note that, until C++14, when declaring a template parameter that's a template itself, the syntax is

    template <template <typename...> class Operation, typename... Args>
    //                               ^ class: you can use typename since C++17
    constexpr bool is_detected_v = boost::is_detected<Operation, Args...>::value;