Search code examples
runtimec++14foldcompile-timec++17

Short circuit dispatching and stop condition in a string-literal to type matcher


I am playing with some piece of code, taken from Avoid if-else branching in string to type dispatching answer from Vittorio Romeo, but rewritten to use with C++14 cause Vittorios version uses C++17 fold expressions. I also thought the rewrite would be a good exercise.

Here is the code:

#include <type_traits>
#include <iostream>
#include <utility>
#include <string>

template<char... Cs>
using ct_str = std::integer_sequence<char, Cs...>;

template<typename T, T... Cs>
constexpr ct_str<Cs...> operator""_cs() { return {}; }

template<typename Name, typename T>
struct named_type
{
    using name = Name;
    using type = T;
};

template<typename... Ts>
struct named_type_list { };

using my_types = named_type_list<
    named_type<decltype("int"_cs), int>,
    named_type<decltype("bool"_cs), bool>,          
    named_type<decltype("long"_cs), long>,
    named_type<decltype("float"_cs), float>,  
    named_type<decltype("double"_cs), double>,
    named_type<decltype("string"_cs), std::string>
>;

template<std::size_t... Is, char... Cs>
constexpr bool same_impl(const std::string& s, 
               std::integer_sequence<char, Cs...>, 
               std::index_sequence<Is...>)  
{
    const char c_arr[] = {Cs...};
    for (std::size_t i = 0; i != sizeof...(Cs); ++i) {
        if (s[i] != c_arr[i]) return false;
    }
    return true;

    //Original C++17 (fold expression)
    //return ((s[Is] == Cs) && ...);
}

template<char... Cs>
constexpr bool same(const std::string& s, std::integer_sequence<char,     Cs...> seq)
{
    std::cout << "checking '" << s << "' against '";
    std::initializer_list<bool>{ bool(std::cout << Cs)... };
    std::cout << "'\n";

    return s.size() >= sizeof...(Cs) 
        && same_impl(s, seq, std::make_index_sequence<sizeof...(Cs)>{});
}

template<typename... Ts, typename F>
void handle(named_type_list<Ts...>, const std::string& input, F&& f)
{
    using expand_type = int[];
    expand_type{ 0, (same(input, typename Ts::name{}) && (f(Ts{}), false), 0)... };

    //(void)std::initializer_list<int> {
    //    ( (same(input, typename Ts::name{}) && (f(Ts{}), false) ), 0)...
    //};

    //Original C++17 (fold expression)
    //( (same(input, typename Ts::name{}) && (f(Ts{}), true) ) || ...);
}

int main(int argc, char** argv) 
{
    const std::string input{"float"};
    handle(my_types{}, input, [](auto t)
    {
        std::cout << typeid(typename decltype(t)::type).name() << "\n";

        // TEST: define std::vector with value_type (matched type "float") and add a few values
        using mtype = typename decltype(t)::type;
        std::vector<mtype> x;
        x.push_back(2.2); // <-- does not compile
    });
    return 0;
}

I assume problem lies in the handle function that seems not to stop the evaluation properly. It should stop at the first invocation of f() in case of a match. Instead, it executes f() in case of a match as expected, but continues executing the remaining types in the named_type_list.

The current code results in this output:

checking 'float' against 'int'
checking 'float' against 'bool'
checking 'float' against 'long'
checking 'float' against 'float'
f
checking 'float' against 'double'
checking 'float' against 'string'

Actually I have no clue how to get that fixed. I tried to rewrite the C++17 fold expression using the std::initializer_list trick and also tried to use an expander (the uncommented part in the handle body. So I guess it is the expression itself not working properly.

Unfortunately I am out of ideas whats really happening at this point, also the fact that I am not experienced with Meta-Programming/Compile-time evaluation.

Another problem arises with an possible use of this code: My use case would be in an XML property reader where I have type/value tags, e.g. <attribute type="double" value="2.5"/>, applying something like the handle function to get the typename from the type attribute value. That type I could use to further process the value.

For this I added within the handle f()-body in main() 3 lines, defining an std::vector with the found type and trying to add a value to it. This code does not compile, g++ responds with

error: no matching function for call to ‘std::vector<std::basic_string<char>, std::allocator<std::basic_string<char> > >::push_back(double)’

I guess this is the mixup out of compile-time and run-time behaviour and it does not work this way and that makes me curious how I could further process/use the matched type.

Thanks for your time on explanation, any help is greatly appreciated!


Solution

  • ||, of course, short-circuits. Your version doesn't. I don't see short-circuiting as essential to correctness here, but if you want, it's easily implemented with an additional bool:

    bool found = false;
    expand_type{ 0, (!found &&
                     same(input, typename Ts::name{}) && 
                     (f(Ts{}), found = true), 0)... };
    

    The second problem is because your handler function must be validly callable for every possible type in the type list, but you can't push_back 2.2 into a std::vector<std::string>. As an example, you might have something that obtains the value as a string, and the handler body could lexical_cast it to mtype.