3

I am working on a project for an Arduino-based sytem (i.e. an embedded system) and have a limited amount of ROM for code to live in.

I have found myself needing several different types of collection classes (e.g List, Stack, Queue). After writing these I noticed that aside from the features that make them behave differently (e.g. Add vs Push vs Enqueue, Pop vs Dequeue) they share a lot of common functionality (e.g. they have the exact same fields).

This got me thinking, if I were to create a base class that contained all of the common functionality of the other classes (let's call it Collection) and made the other classes inherit from it, and have them implement only the functionality that differs, would that result in less compiled code?

I'm expecting that it will because the child classes can all refer to the parent's implementations of the common functions, but I could be wrong so I'd rather ask before I attempt this change.


Ok, to clarify this further since people seem to be misinterpreting, consider these hypothetical classes:

class A
{
protected:
    int value;
public:
    void assign(int other)
    {
        this->value = other;
    }
}

class B : public A
{
public:
    void assignAdd5(int other)
    {
        this->value = other + 5;
    }
}

And then the code:

A a = A();
B b = B();

a.assign(4);
b.assign(4);

I am expecting that assign refers to the same method in both cases (and thus the same compiled block of code) despite the different types because B is a subclass of A. If this is the case then having classes with similar functionality all inherit from a single base class implementing the similarities would produce less compiled code than making each class implement the functionality separately because each child class would be using the same function.

The information I provided regarding the situation that prompted this question was merely background, I am in whether this is the case with any compiler, not just the one I'm using (and I fully appreciate that the answer may be "it's possible that some compilers do this, but not all of them do", and that's a perfectly acceptable answer).

Pharap
  • 3,826
  • 5
  • 37
  • 51
  • 1
    why dont you try and see? – 463035818_is_not_an_ai Mar 24 '17 at 18:44
  • @tobi303 Because it's time consuming, someone may already know the answer and I couldn't already find an answer to this question on SO so I thought maybe other people might benefit from an answer. – Pharap Mar 24 '17 at 18:48
  • 2
    it wasnt meant as critisism, but eventually the numbers may differ for your specific case. I got curious now and trying to get an example where it makes a difference – 463035818_is_not_an_ai Mar 24 '17 at 18:53
  • [this](http://stackoverflow.com/questions/3527187/what-makes-exes-grow-in-size) is somehow related. Unfortunately the accepted answer adresses windows only where the size of the executable doesnt matter too much – 463035818_is_not_an_ai Mar 24 '17 at 18:59
  • @tobi303 I understand that the answer may well be that it depends on the compiler, but that's still a valid answer and it would be nice to know if there are compilers where this will reduce resultant code size or if there's some rule in the C++ standard that allows/denies this from happening. – Pharap Mar 24 '17 at 19:01
  • I am not too familiar with the standard, but I would be surprised if it mentions the size of the executable – 463035818_is_not_an_ai Mar 24 '17 at 19:02
  • @tobi303 It almost certainly won't, but it might explicitly state some rule that has a knock-on effect. E.g. 'implementations must ensure that child classes have their own copy of the parent's methods for *insert non-obvious reason here*'. Any reduction in code size would come from the re-use of the parent's methods, and if there's a rule that prevents the child classes using the parent's methods (e.g. they must have their own copies for some reason) then there won't be a code size reduction. – Pharap Mar 24 '17 at 19:05
  • 1
    Why have 3 types at all? One type. It supports push/pop back and front. It supports adding in the middle. When used as a `Queue` you ... don't call the other methods. Only write a new type when a tradeoff for one of the types makes the solution for another type suboptimal, *and* that cost is worth supporting & shipping more code. – Yakk - Adam Nevraumont Mar 24 '17 at 19:51
  • what IDE are you using? – wonko realtime Mar 24 '17 at 20:26
  • @Pharap there is no such rule, C++ tries to be as efficient as possible. The only time you might see excess code is when you're using template classes, as the code must be duplicated for each template type. – Mark Ransom Mar 24 '17 at 20:27
  • This is platform dependent, some compilers may optimize some unused code away, some may not. You will have to be more specific. If you are working with your own data types and having memory issues, make sure that the order of the member variables is adequate to prevent the struct from taking up more space than it should due to struct padding. – senex Mar 24 '17 at 20:41
  • @wonkorealtime I'm using the Arduino Software IDE 1.8.2, but am not just interested in my specific use-case but whether this is possible at all (i.e. even if some compilers don't do it). – Pharap Mar 24 '17 at 21:23
  • @AlexHG My situation is not that I'm running out of memory and have to change how things work to make room for more code, my situation is that I am near the begining and would like to know sooner rather than later if I could save memory later down the line by using iheritance as specified in the question. I am also interested in the hypothetical answer rather than something specific to my circumstance so that I know whether it's possible or not. If the answer is "no it's not possible to save code memory with inheritance" then I will no in future to not even consider it. – Pharap Mar 24 '17 at 21:42
  • @Yakk Because it makes the intent clear and because some of the specific operations conflict. For example, it is considered 'illegal' for a `Queue` in this case to have things inserted half way along the container (jumping the `Queue` so to speak), but for a `List` that behaviour is actually desired. Hence a monolithic god-collection that does both would be relying on the programmers to not touch `insert` if they want to use is as a `Queue` and would have to forbid it in comments rather than at the language level. – Pharap Mar 24 '17 at 21:55
  • In your example most half decent compilers would inline away the calls to assign anyway, but assuming that there's no inlining possible for assign and that you avoid vtables completely, you could achieve a similar but more composition like reusing by doing like the compiler which passes the this to assign (assign(A*, int)) and create an assign(int&, int) free func (which is also assumed to be not inlinable) and invoke that though proxy member funcs (which most likely would be inlined) like A::assign(int other) { ::assign(this->value, other) } wherever needed. – wonko realtime Mar 25 '17 at 00:29

2 Answers2

1

I cannot give a definite answer, but I want to share my findings. Actually I isnt that easy to measure the difference as I expected. Dont take the code too serious, I was just trying random stuff to see if it has any impact...

#include <iostream>
#define DERIVED
#define BASE_STUFF     int a,b,c,d,e,f,g,h,i,k,m,n,o,p,q,r,s,t,u,v,w,x,y,z; \
                       double foo1(int x){return x;}                        \
                       double foo2(int x){return x;}                        \
                       double foo3(int x){return x;}                        \
                       double foo4(int x){return x;}                        \
                       Base*  foo5(Base* x){return x;}                      \
                       FPTR   foo5(FPTR a,FPTR b,FPTR c){return a;}

typedef double (*FPTR)(int,int,double);

struct Base { BASE_STUFF };

#ifdef DERIVED
struct Derived : Base{double a0,a1,a2,a3,a4,a5,a6,a7;};
#endif

#ifndef DERIVED
struct Derived {
    double a0,a1,a2,a3,a4,a5,a6,a7;
    BASE_STUFF
};
#endif

int main(int argc, char *argv[])
{
    Base b;
    b.x = 1;
    std::cout << b.foo1(b.x);
    std::cout << b.foo5(&b)->foo2(b.a);
    Derived n;
    n.a0 = 2;
    std::cout << n.foo1(n.a0);
}

I have to check again for the numbers, but to my surprise the executable was smaller when using iostreams as compared to c-style prinft.

When I compile this with (g++4.9.2)

g++ main.cpp -Os -fno-exceptions -s

I get an executable of 13.312 kB. And that is independent of DERIVED being defined or not. However, when I compile it with

g++ main.cpp -Os -fno-exceptions

the size is 43.549 kB. Again independent of DERIVED being defined or not.

My conclusions are

  • my example isnt good to measure the difference
  • ...however, experimenting with compiler flags results in huge differences, while inheritance or not makes little difference
  • I would not change the design with the aim to reduce the code size. Write your code so that it is easy to read, write, and use and use compiler flags if you need to squeeze some kB out of your exe (see also here)
Community
  • 1
  • 1
463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
0

Write a monolithic container that does it all.

Inherit from it privately. Use using to bring methods you want to expose into view.

At most some extremely short methods will be created for dtor/ctor by the compiler.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524