7

I am not sure if the title is correct but here is my problem/question:

I would like to use metaprogramming in order to create functions for a specific expression. For example lets say that we have this code:

template<typename T1, typename T2>
struct plus{
    T1 func(T1 in1, T2 in2){ return in1 + in2; }
};

template<typename T1, typename T2, typename T3, typename expr>
struct wrap{

    /* contain a func that can evaluate the expr */
};

and the programmer will write the code bellow in order to create a function for an expression:

wrap<int,int,int,plus<plus<int,int>,int> >::func(1,2,3); /*result should be 6*/

Is this possible?

Thank you.

2
  • 2
    Possible: Yes. Advisable: Disputable. While there are many people out there who love template metaprogramming, there are also people who try to avoid it. The problem is, that you never see the expanded version of your templates unless a compiler error spits it out at you. Especially the expression templates that solve your question are notorious for producing a horrible mess of unreadable error messages on seemingly innocent code. My advise: If you want to generate code, be honest about it and write a script (python, perl, m4, ...). That way you can read both, script and generated code. Commented Oct 31, 2014 at 20:55
  • what about simply writing auto func= [](auto x,auto y,auto z) {return x+y+z;};, followed by a call func(1,2,3)? Commented Nov 2, 2014 at 12:34

2 Answers 2

7
#include <utility>
#include <tuple>
#include <cstddef>

struct arg
{
    template <typename Arg1>
    static constexpr decltype(auto) apply(Arg1&& arg1)
    {
        return std::forward<Arg1>(arg1);
    }

    static constexpr std::size_t arity = 1;
};

template <typename Type, Type value>
struct constant
{    
    static constexpr decltype(auto) apply()
    {
        return value;
    }

    static constexpr std::size_t arity = 0;
};

template <typename Lhs, typename Rhs>
struct plus
{
    template <typename... Args>
    static constexpr decltype(auto) apply(Args&&... args)
    {
        return _apply(std::make_index_sequence<Lhs::arity>{}, std::make_index_sequence<Rhs::arity>{}, std::tuple<Args&&...>(std::forward<Args>(args)...));
    }

    template <typename Tuple, std::size_t... Arity1, std::size_t... Arity2>
    static constexpr decltype(auto) _apply(std::index_sequence<Arity1...>, std::index_sequence<Arity2...>, Tuple&& args)
    {
        return Lhs::apply(static_cast<typename std::tuple_element<Arity1, Tuple>::type>(std::get<Arity1>(args))...)
             + Rhs::apply(static_cast<typename std::tuple_element<Lhs::arity + Arity2, Tuple>::type>(std::get<Lhs::arity + Arity2>(args))...);
    }

    static constexpr std::size_t arity = Lhs::arity + Rhs::arity;
};

template <typename Lhs, typename Rhs>
struct multiply
{
    template <typename... Args>
    static constexpr decltype(auto) apply(Args&&... args)
    {
        return _apply(std::make_index_sequence<Lhs::arity>{}, std::make_index_sequence<Rhs::arity>{}, std::tuple<Args&&...>(std::forward<Args>(args)...));
    }

    template <typename Tuple, std::size_t... Arity1, std::size_t... Arity2>
    static constexpr decltype(auto) _apply(std::index_sequence<Arity1...>, std::index_sequence<Arity2...>, Tuple&& args)
    {
        return Lhs::apply(static_cast<typename std::tuple_element<Arity1, Tuple>::type>(std::get<Arity1>(args))...)
             * Rhs::apply(static_cast<typename std::tuple_element<Lhs::arity + Arity2, Tuple>::type>(std::get<Lhs::arity + Arity2>(args))...);
    }

    static constexpr std::size_t arity = Lhs::arity + Rhs::arity;
};

Test:

int main()
{
    // (1 + 2) + 3 = 6
    std::cout << plus<plus<arg, arg>, arg>::apply(1, 2, 3) << std::endl;

    // (a + 5) + (2 * 6) = 9 + 12 = 21
    int a = 4;
    std::cout << plus<plus<arg, arg>, multiply<arg, constant<int, 6>>>::apply(a, 5, 2) << std::endl;

    // ((1 * 2) * 3) * 4 = 24
    std::cout << multiply<multiply<multiply<arg, arg>, arg>, arg>::apply(1, 2, 3, 4) << std::endl;

    // 2 + (4 * 5) = 22
    static_assert(plus<arg, multiply<arg, arg>>::apply(2, 4, 5) == 22, "!");
}

Output:

6
21
24

DEMO 1


The above solution can be improved so that introducing new functors requires less effort, and the declarations themselves are more readable, like below:

#include <iostream>
#include <utility>
#include <tuple>
#include <cstddef>

template <std::size_t Arity>
struct expression
{    
    static constexpr std::size_t arity = Arity;
};

template <typename Expr, typename Rhs>
struct unary_expression : expression<Rhs::arity>
{    
    template <typename... Args>
    static constexpr decltype(auto) apply(Args&&... args)
    {
        static_assert(sizeof...(Args) == unary_expression::arity, "Wrong number of operands!");
        return Expr::eval(Rhs::apply(std::forward<Args>(args)...));
    }
};

template <typename Expr, typename Lhs, typename Rhs>
struct binary_expression : expression<Lhs::arity + Rhs::arity>
{
    template <typename... Args>
    static constexpr decltype(auto) apply(Args&&... args)
    {
        static_assert(sizeof...(Args) == binary_expression::arity, "Wrong number of operands!");
        return _apply(std::make_index_sequence<Lhs::arity>{}, std::make_index_sequence<Rhs::arity>{}, std::tuple<Args&&...>(std::forward<Args>(args)...));
    }

    template <typename Tuple, std::size_t... Arity1, std::size_t... Arity2>
    static constexpr decltype(auto) _apply(std::index_sequence<Arity1...>, std::index_sequence<Arity2...>, Tuple&& args)
    {
        return Expr::eval(Lhs::apply(static_cast<typename std::tuple_element<Arity1, Tuple>::type>(std::get<Arity1>(args))...),
                          Rhs::apply(static_cast<typename std::tuple_element<Lhs::arity + Arity2, Tuple>::type>(std::get<Lhs::arity + Arity2>(args))...));
    }
};

struct arg : expression<1>
{
    template <typename Arg1>
    static constexpr decltype(auto) apply(Arg1&& arg1)
    {
        return std::forward<Arg1>(arg1);
    }
};

template <typename Type, Type value>
struct constant : expression<0>
{    
    static constexpr decltype(auto) apply()
    {
        return value;
    }
};

template <typename Rhs>
struct negate : unary_expression<negate<Rhs>, Rhs>
{
    template <typename Arg1>
    static constexpr decltype(auto) eval(Arg1&& arg1)
    {
        return -std::forward<Arg1>(arg1);
    }
};

template <typename Lhs, typename Rhs>
struct plus : binary_expression<plus<Lhs, Rhs>, Lhs, Rhs>
{
    template <typename Arg1, typename Arg2>
    static constexpr decltype(auto) eval(Arg1&& arg1, Arg2&& arg2)
    {
        return std::forward<Arg1>(arg1) + std::forward<Arg2>(arg2);
    }
};

template <typename Lhs, typename Rhs>
struct minus : binary_expression<minus<Lhs, Rhs>, Lhs, Rhs>
{
    template <typename Arg1, typename Arg2>
    static constexpr decltype(auto) eval(Arg1&& arg1, Arg2&& arg2)
    {
        return std::forward<Arg1>(arg1) - std::forward<Arg2>(arg2);
    }
};

template <typename Lhs, typename Rhs>
struct multiply : binary_expression<multiply<Lhs, Rhs>, Lhs, Rhs>
{
    template <typename Arg1, typename Arg2>
    static constexpr decltype(auto) eval(Arg1&& arg1, Arg2&& arg2)
    {
        return std::forward<Arg1>(arg1) * std::forward<Arg2>(arg2);
    }
};

int main()
{    
    // (1 + 2) + 3 = 6
    std::cout << plus<plus<arg, arg>, arg>::apply(1, 2, 3) << std::endl;

    // ((a + 5) + (2 * 6)) - 5 = 16
    int a = 4;
    std::cout << minus<plus<plus<arg, arg>, multiply<arg, constant<int, 6>>>, constant<int, 5>>::apply(a, 5, 2) << std::endl;

    // ((1 * 2) * 3) * 4 = 24
    std::cout << multiply<multiply<multiply<arg, arg>, arg>, arg>::apply(1, 2, 3, 4) << std::endl;

    // -((3 * 4) + (5 - 6)) = -11
    static_assert(negate<plus<multiply<arg, arg>, minus<arg, arg>>>::apply(3, 4, 5, 6) == -11, "!");
}

DEMO 2

Sign up to request clarification or add additional context in comments.

7 Comments

+1. As a further abstraction, you could avoid hard-coding the plus and minus stuff and allow for any binary function (which is constexpr). This you could avoid the mostly identical code (as they differ only by + and *).
@davidhigh this is what I did in DEMO 2, where common code was moved to binary_expression class
@Eldrad: you surely want to use this for other types than int, I guess? Because you know that, e.g., wrap<plus<plus<value, value>, multiply<value, constant<int, 6>>>>::func(a, 5, 2) is equivalent to (a + 5) + (2 * 6), whereas the latter is obviously much easier to write ...
@davidhigh you can't expand neither (a + 5) + (2 * 6), nor [](auto x,auto y,auto z) {return x+y+z;} at compile time through metaprogramming techniques, possibly operating on them as simple template's parameters. I think this is what the question is all about.
Thanks for the info. But then, instead of pages of code a single constexpr implementing the expression would do (or possibly more than one, if the expression becomes too complex). I see no advantages in not using this ... at least not for fundamental types. Are there some?
|
2

Absolutely. These are called "expression templates" and you can find the SO highlights here.

I worked on the POOMA system for parallel programming back in the late 90s. Not sure if its been updated to modern standards, but I see that it is still available online here. Underlying POOMA was an "Expression Template Engine" called PETE that could be repurposed for other evaluation engines. PETE is described here. All of that work would be much simpler with C++ 11 and I'm sure there are similar efforts out there that use these newer capabilities.

2 Comments

Could you give a concise sample code, based on what the OP has posted, please?
I'll take a look after a bit - working right now. :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.