Search code examples
c++overriding

Problems with understanding how override method works in c++


Sorry for all mistakes, English is not my native language. I'm relatively new in c++ and I have problem with understanding If I have a method that should be overridden and requires no parameters but I already have function that does exactly same job but requires two parameters should I rewrite my old method or this problem can be solved without rewriting? This function should be overridden:

virtual void read() = 0;

This is working function with two parameters required:

void read(rclcpp::Time time, rclcpp::Duration period){
    ...
}

Solution

  • The signature of a function in C++ can be thought of as part of its name. In fact, the way C++ was implemented using an existing infrastructure of library tools and linkers was to make the signature part of the name.

    First let's take a look at how C handles "overloaded" functions. As you may know, function overloading is not possible in C, and here is why.

    I have three functions with the same name but different parameters in three different files (and they must be in different files because having them in the same file is a forbidden redefinition of the same name in C):

    $  for f in fv fi fii; do echo $f.cpp:; cat $f.cpp; done
    fv.cpp:
    int func(){ return 0; }
    
    fi.cpp:
    int func(int i){ return 1; }
    
    fii.cpp:
    int func(int i1, int i2){ return 2; }
    
    

    I compile them to object files with gcc, forcing gcc to compile them as C code with -x c, and then take a look at the "symbols" (here: functions) each one defines with the tool nm:

    $ gcc -c -x c  fv.cpp fi.cpp fii.cpp
    $ nm --extern-only  fv.o fi.o fii.o
    
    fv.o:
    0000000000000000 T func
    
    fi.o:
    0000000000000000 T func
    
    fii.o:
    0000000000000000 T func
    
    

    All three files define the same function func, regardless of their parameters. Accordingly, code using one of the functions communicates only an undefined symbol func to the linker. Here is a minimal program using a func:

    $ cat callfunc.cpp
    #include <stdio.h>
    
    int func(int, int);
    
    int main()
    {
      printf("%d\n", func(1, 1));
    }
    

    We can compile it into an object file and inspect the undefined symbols it uses:

    $ gcc -x c -c callfunc.cpp
    $ nm --extern-only --undefined-only callfunc.o
                     U __main
                     U func
                     U printf
    

    Indeed, besides the standard library function printf and the internal __main it just needs a function func. Consequently, the linker will accept any of the three functions we have! Let's see:

    $ ls *.o
    callfunc.o  fi.o  fii.o  fv.o
    $ gcc -o callfunc callfunc.o fv.o && ./callfunc
    0
    $ gcc -o callfunc callfunc.o fi.o && ./callfunc
    1
    $ gcc -o callfunc callfunc.o fii.o && ./callfunc
    2
    

    Indeed, each time the function defined in the respective file is linked and an executable is produced that uses it. The linker knows nothing about the parameters. This is a C legacy. It is also bug prone: Using a function with different parameters than it is expecting causes undefined behavior; for example, it may corrupt your stack. With our short programs we were just lucky.

    This also means that we cannot link more than one object file that defines a function with the same name, even if it has a different set of parameters: The linker then has multiple definitions of the same symbol, which is by default rejected because it's likely an error (which one should it link to!?):

    $ gcc -o callfunc callfunc.o fv.o fi.o
    C:/Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: fi.o:fi.cpp:(.text+0x
    0): multiple definition of `func'; fv.o:fv.cpp:(.text+0x0): first defined here
    collect2.exe: error: ld returned 1 exit status
    

    As mentioned above, the situation in C++ is fundamentally different: We can overload functions, that is, have functions with the same name but different parameter sets. Your read() and read(rclcpp::Time time, rclcpp::Duration period) are examples for that. This is not overriding (which replaces a function with the same signature in a derived class with a different implementation) but overloading (which provides several functions with the same name but different signatures).

    Because the linker lacks the ability to distinguish different signatures the compiler uses a hack: It produces names which are distinct for different signatures. The linker then sees entirely unrelated functions and chooses the required symbol, as always. It cannot link against one of the other object files because they contain, as far as the linker is concerned, unrelated symbols.

    That the internally produced function names are different reveals the compile-time nature of function overloading and overload resolution: Because it is clear at compile time which overloaded function is called in an expression, one could as well use distinct function names right away. Overloading is simply syntactic sugar and has no function beyond communicating that the programmer thought that a group of functions belongs together. (This is very different from overriding virtual member functions which is an essential languages feature for facilitating polymorphism in C++.)

    So let's see how our functions look in C++:

    $ g++ -c fv.cpp fi.cpp fii.cpp
    $ nm --extern-only  fv.o fi.o fii.o
    
    fv.o:
    0000000000000000 T _Z4funcv
    
    fi.o:
    0000000000000000 T _Z4funci
    
    fii.o:
    0000000000000000 T _Z4funcii
    

    It's easy to see that each int parameter is encoded via an "i" suffix, while a void function is suffixed with a "v".

    The corresponding unresolved symbol in callfunc.o is correspondingly named:

    $ g++ -c callfunc.cpp
    $ nm --extern-only --undefined-only callfunc.o
                     U __main
                     U _Z4funcii
                     U printf
    
    

    The linker now expects a function with that name. That's Abagnale, not Abagnolee, not Abagnalee, but Abagnale, which the linker will let you know in no unclear terms ;-).

    $ g++ -o callfunc callfunc.o fii.o && ./callfunc
    2
    

    Trying to satisfy this unresolved symbol by linking with one of the other object files fails:

    $ g++ -o callfunc callfunc.o fi.o && ./callfunc
    C:/Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: callfunc.o:callfunc.c
    pp:(.text+0x18): undefined reference to `func(int, int)'
    collect2.exe: error: ld returned 1 exit status
    $ g++ -o callfunc callfunc.o fv.o && ./callfunc
    C:/Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: callfunc.o:callfunc.c
    pp:(.text+0x18): undefined reference to `func(int, int)'
    collect2.exe: error: ld returned 1 exit status
    

    Now if you have a function read() in your base class and a function read(rclcpp::Time time, rclcpp::Duration period) in a derived class you are not overriding anything: The second function is entirely unrelated to the first one. You could as well have named it, say readForTimeAndPeriod, as far as C++ is concerned.