Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to direct "c.foo()+c.boo()" to "c.foo_and_boo()"?

Tags:

c++

There is a class C with methods foo, boo, boo_and_foo and foo_and_boo. Each method takes i,j,k and l clock cycles respectively, where i < j, k < i+j and l < i+j.

class C
{
public:
  int foo() {...}
  int boo() {...}
  int boo_and_foo() {...}
  int foo_and_boo() {...} };

In code one might write:

C c;
...
int i = c.foo() + c.boo();

But it would be better to have:

int i = c.foo_and_boo();

What changes or techniques could one make to the definition of C, that would allow similar syntax of the original usage, but instead have the compiler generate the latter. Note that foo and boo are not commutative.

Idea 1 Thanks the comments!

I was thinking creater a cHelper class, and a cOps class with string member ops, so that after have cHelper ch;, we can write int i = ch.foo() + ch.boo();

  1. ch.foo() does nothing but returns: cOps("foo")
  2. ch.boo() also only returns: cOps("boo")
  3. overload "+" operator for cOps class so that cOps("foo") + cOps("boo") actually returns cOps("foo,boo")
  4. overload the int type conversion operator of cOps so that int i = cOps("foo") ; actually calls c.foo(); while int i = cOps("foo,boo") ; actually calls c.foo_and_boo();

A bit ugly ....

like image 464
athos Avatar asked Nov 19 '25 03:11

athos


1 Answers

The objective sounds rather similar to what Todd Veldhuizen pioneered many moons ago for Blitz++: aggregating operations effectively into an expression tree to effectively change the order of computations. In the case of Blitz++ it was done for matrix operations. The technique is known as Expression Templates.

The overall idea would not be to aggregate a string and its operations, though, but rather to encode the necessary operations in the type returned. Similar to what is described in the question the evaluation of the expression is delayed until they are needed. By using specializations for specific operations the compiler would be made to choose the appropriate combations. It could look somewhat like this:

#include <iostream>

namespace C_ops { struct C_tag {}; }

struct C_foo;
struct C_bar;

class C: C_ops::C_tag {
    int value;
public:
    explicit C(int value): value(value) {}
    C_foo foo() const;
    C_bar bar() const;

    int actual_foo() const {
        std::cout << "compute foo(" << value << ")\n";
        return value;
    }
    int actual_bar() const {
        std::cout << "compute bar(" << value << ")\n";
        return value;
    }
    int foo_and_bar(C const& r) const {
        std::cout << "compute foo_and_bar(" << this->value << ", " << r.value << ")\n";
        return this->value;
    }
    int bar_and_foo(C const& r) const {
        std::cout << "compute bar_and_foo(" << this->value << ", " << r.value << ")\n";
        return this->value;
    }
};

struct C_foo: C_ops::C_tag {
    C const& c;
    C_foo(C const& c): c(c) {}
    operator int() const { return c.actual_foo(); }
};
struct C_bar: C_ops::C_tag {
    C const& c;
    C_bar(C const& c): c(c) {}
    operator int() const { return c.actual_bar(); }
};

C_foo C::foo() const { return C_foo(*this); }
C_bar C::bar() const { return C_bar(*this); }

template <typename L, typename R>
struct C_add;
 template <>
 struct C_add<C_foo, C_bar> {
    C_foo l;
    C_bar r;
    C_add(C_foo const& l, C_bar const& r): l(l), r(r) {}
    operator int() const { return l.c.foo_and_bar(r.c); }
};
template <>
struct C_add<C_bar, C_foo> {
    C_bar l;
    C_foo r;
    C_add(C_bar const& l, C_foo const& r): l(l), r(r) {}
    operator int() const { return l.c.bar_and_foo(r.c); }
};
// more specializations, e.g., to deal with C on the LHS

namespace C_ops {
    template <typename L, typename R>
    C_add<L, R> operator+(L const& l, R const& r) {
        return C_add<L, R>(l, r);
    }
}

template <typename... T> void use(T const&...) {}
int main()
{
    C c0(0), c1(1);
    int r0 = c0.foo();
    int r1 = c0.bar();
    int r2 = c0.foo() + c1.bar();
    int r3 = c0.bar() + c1.foo();
    std::cout << "done\n";
    use(r0, r1, r2, r3); // make them used to not have them optimized away (for now)
}

With the setup above, the four expressions you mentioned would be covered. There are probably more specializations needed, e.g., to all c + c.foo(), c + c, etc. but the overall principle doesn't really change. You may want to protect actual_foo() and actual_bar() from being accessed and making the respective classes needing to call the friends.

The need for the extra namespace C_ops is merely to provide "catch-all" operators which hijack operator+() for all types, as long as they happen to be loooked up in this namespace. To do so, there is also a tag-type which has no other purpose than directing ADL (argument dependent look-up) to look into this namespace.

This is quite a cute technique which worked rather reliably with C++03: since the expressions are evaluated when the target type (in the above case int) is needed and C++03 didn't really allow for capturing the intermediate type while potentially involved temporary object were not around any more (the only was to catch the expression type in a deduced context would be when passing it to a function template) there were no life-time issues. With C++11 it is to capture the type of objects using auto which would capture the expression type, not evaluated result, there is probably some need to think properly about life-time of involved objects. The above example just used const objects but you'll probably need to be more careful and prevent certain temporary objects from being capture, primarily objects of type C. From your descriptions it isn't clear whether these would be expensive objects: the ideal solution would be to capture all objects by value but that may be too expensive.

like image 177
Dietmar Kühl Avatar answered Nov 21 '25 17:11

Dietmar Kühl