3
\$\begingroup\$

Motivated due to the fact, that the avr-g++ places the vtables in RAM, I wrote a replacement using static polymorphy.

Consider the following example:

volatile uint8_t x;

struct IX {
    virtual void f() const = 0;
//    virtual ~IX() = default; // need delete
};

struct A : public IX {
    const uint8_t v = 0;
    void f() const override {
        x = v;
    }
};

struct B : public IX {
    const uint8_t v = 1;
    void f() const override {
        x = v;
    }
};

struct C : public IX {
    const uint8_t v = 2;
    void f() const override {
        x = v;
    }
};

volatile uint8_t index = 2;

int main() {
    A a;
    B b;
    C c;
    const std::array<const IX*, 3> cc{&a, &b, &c};

    cc[index]->f();

    while(true) {}
}

Here we have some types A, B and C implementing an interface IX and placing pointers in the array cc. Then we call the virtual function f() for a specific instance. (Using this on a small µC like the AVRs, there is a "waste" of RAM, since the vtables are placed in RAM and each object contains a vptr, and a performance penalty due to the indirect call of f().

So I looked for an alternative solution in this case: the simplest way is to use an heterogenous container like std::tuple and write a switch-statement:

const std::tuple<A, B, C> t;

auto f = [](const auto& v) {
    v.f();
};

switch (index) {
case 0:
    f(std::get<0>(t));
    break;
case 1:
    f(std::get<1>(t));
    break;
case 2:
    f(std::get<2>(t));
    break;
default:
    assert(false);
    break;
}

This yields to optimale machine-code but it is an unflexible solution. So I wrote a metafunction to call f() for a specific element of the tuple:

const std::tuple<A, B, C> t;

Meta::visitAt(t, index, [](const auto& v){v.f();});

And the implementation looks like:

namespace Meta {
    namespace detail {
        template<uint8_t  N>
        struct visit {
            template<typename T, typename F>
            static void at(T& tuple, uint8_t index, const F& f) {
                if (index == (N - 1)) {
                    f(std::get<N - 1>(tuple));
                }
                else {
                    visit<N - 1>::at(tuple, index, f);
                }
            }

        };
        template<>
        struct visit<0> {
            template<typename T, typename F>
            static void at(T&, uint8_t , const F&) {
                assert(false);
            }
        };

        template<typename T, typename F, size_t... I>
        void all(const T& tuple, const F& f, std::index_sequence<I...>) {
            (f(std::get<I>(tuple)), ...);
        }

    }
    template<typename... T, typename F>
    void visitAt(const std::tuple<T...>& tuple, uint8_t index, const F& f) {
        detail::visit<sizeof...(T)>::at(tuple, index, f);
    }
    template<typename... T, typename F>
    void visitAt(std::tuple<T...>& tuple, uint8_t index, const F& f) {
        detail::visit<sizeof...(T)>::at(tuple, index, f);
    }
    template<typename... T, typename F>
    void visit(const std::tuple<T...>& tuple, const F& f) {
        detail::all(tuple, f, std::make_index_sequence<sizeof...(T)>{});
    }
}

This works very well in my scenarios, yet is obviously limited to static containers (like std::tuple). There is also a for-each-like iteration Meta::visit().

My question is: are there any other drawbacks / limitations with this approach?

Are there any improvements?

\$\endgroup\$
  • \$\begingroup\$ static polymorphism means you use templates and ducktyping everywhere to propagate the type (and associated functions) down to where they are called. \$\endgroup\$ – ratchet freak Jul 7 '17 at 11:02
  • \$\begingroup\$ Technically true performance penalty due to the indirect call of f() but in real life that penalty is insignificantly small and will be drowned out by other CPU activity. I challenge you to measure it. \$\endgroup\$ – Martin York Jul 7 '17 at 16:20
  • \$\begingroup\$ drawbacks: You are trying to solve a problem that does not really exist. \$\endgroup\$ – Martin York Jul 7 '17 at 16:23
  • \$\begingroup\$ @LokiAstari: the main penalty is the waste of RAM for small µC. \$\endgroup\$ – wimalopaan Jul 7 '17 at 16:27
  • \$\begingroup\$ @wimalopaan the above code has size. How big is the above code? On small µC I don't expect you will have many objects running on the fly (a couple at a time). So if we look at the above example. The code will be generated for every version of the tuple. So unless the code is smaller than size of the tuple multiplied by the size of the pointer then you have failed. \$\endgroup\$ – Martin York Jul 7 '17 at 17:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.