2

Motivation, if it helps: : I have struct member functions that are radial-basis-function kernels. They are called 1e06 x 15 x 1e05 times in a numerical simulation. Counting on devirtualization to inline virtual functions is not something I want to do for that many function calls. Also, the structs (RBF kernels) are already used as template parameters of a larger interpolation class.

Minimal working example

I have a function g() that is always the same, and I want to reuse it, so I pack it in the base class.

The function g() calls a function f() that is different in derived classes.

I don't want to use virtual functions to resolve the function names at runtime, because this incurs additional costs (I measured it in my code, it has an effect).

Here is the example:

#include <iostream>

struct A 
{
    double f() const { return 0; };

    void g() const
    {
        std::cout << f() << std::endl; 
    }
};

struct B : private A
{
    using A::g; 
    double f() const { return 1; };
};

struct C : private A
{
    using A::g;
    double f() const { return 2; };
};

int main()
{
    B b; 
    C c; 

    b.g(); // Outputs 0 instead of 1 
    c.g(); // Outputs 0 instead of 2
}

I expected the name resolution mechanism to figure out I want to use "A::g()", but then to return to "B" or "C" to resolve the "f()" function. Something along the lines: "when I know a type at compile time, I will try to resolve all names in this type first, and do a name lookup from objects/parents something is missing, then go back to the type I was called from". However, it seems to figure out "A::g()" is used, then it sits in "A" and just picks "A::f()", even though the actual call to "g()" came from "B" and "C".

This can be solved using virtual functions, but I don't understand and would like to know the reasoning behind the name lookup sticking to the parent class when types are known at compile time.

How can I get this to work without virtual functions?

tmaric
  • 5,347
  • 4
  • 42
  • 75
  • To make polymorphism work in C++, you first of all need to make the polymorphic functions `virtual`. Then you need to use pointers or references to the *base* class, as in `A* b = new B;` If you want compile-time polymorphism there are ways around that, but it's a lot more work and makes your code much more complicated (and therefore harder to read, understand and maintain). – Some programmer dude Mar 13 '19 at 11:32
  • It would be pretty confusing if A would be a large class, sort of hidden (e.g. from a library) and someone that extends it implements some random function that because of luck or coding standards matches something from A. The whole behavior of A might suddenly change. – Paul92 Mar 13 '19 at 11:33
  • @Paul92: if A is in a library, and I extend B in my library with a member function that shadows a function from A, and the call resolves to this new function in B, that is exactly what I would expect to happen. :) – tmaric Mar 13 '19 at 11:37
  • Also remember that inheritance is a *one way* relationship. If you call a member function in `A`, then it doesn't really know anything about possible child-classes. Virtual dispatch is a workaround for that problem, which solves it at run-time. – Some programmer dude Mar 13 '19 at 11:37
  • @Someprogrammerdude: at the call site, I am calling member functions from `B` and `C`, so there would be no problem for the name resolution to just remember where `g()` was called from, resolve it in the parent, and switch back. It enters `B` and `C` first, then finds `using::`, jumps to `A` and sits there. – tmaric Mar 13 '19 at 11:37
  • If you use virtual, and you have small functions, everything should be inlined, so there would be no virtual dispatch at all for your example. If you have bigger functions, then I doubt that it matters whether the function is virtual or not. Calling virtual functions is fast, it doesn't work like "resolve the function names at runtime". There is no naming resolve at runtime at all. – geza Mar 13 '19 at 11:42
  • By the way, I'm very curious as to *why* you don't want to use standard polymorphism with `virtual` functions? Is it just plain curiosity, or is there a *real* problem in the background that you want to solve? If it's curiosity, then please state so in your question. If there's a real problem lurking behind, then please ask directly about that problem instead. – Some programmer dude Mar 13 '19 at 11:45
  • @geza: https://stackoverflow.com/q/48906338/704028 : devirtualization doesn't always work. – tmaric Mar 13 '19 at 11:45
  • @Someprogrammerdude: the struct member functions are radial-basis-function kernels, that are called 1e06 x 15 x 1e05 times in a numerical simulation. Counting on devirtualization to inline virtual functions is not something I want to do for that many function calls. Also, the structs (RBF kernels) are already used as template parameters of a larger interpolation class. All of this does not help the question at all - my motive for the question does not impact its solution. – tmaric Mar 13 '19 at 11:48
  • And note that doing `using A::g;` doesn't make the function a member of the class you do it in, it just adds the symbol so it can be found (and overrides the `private` inheritance). Talking about private inheritance, why do you use it? It's usually a form of composition, which often can be solved in other ways. – Some programmer dude Mar 13 '19 at 11:49
  • @Someprogrammerdude: exactly my point as well. So the name lookup mechanism *knows* it is fetching `g()` *from a parent*, *in a derived class*. Why then not just fetch it and stay in the derived, *before* trying to look up `f()`? – tmaric Mar 13 '19 at 11:50
  • 1
    That is very crucial information that should have been in the question itself from the very beginning. We really shouldn't have to drag information from you like this in comments. Knowing *why* you want to do something is really crucial for out understanding, and how we will and can help you. Just asking about a solution, without mentioning the problem it's supposed to solve, makes a question an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – Some programmer dude Mar 13 '19 at 11:51
  • @tmaric: evaluating RBF functions likely takes much more time than the time difference between virtual and non-virtual function calls. Put your whole problem here please, because it is not OK to say "devirtualization doesn't always work". Put your exact case here, so we can check whether devirtualization works or not. – geza Mar 13 '19 at 11:55
  • @geza: I can't upload my not yet open source library. The model describes exactly what I want, and there is already an answer based on templates, doing the work at compile time. – tmaric Mar 13 '19 at 11:57
  • @tmaric: your question is very vague at some points. The current answer modifies your model, as it doesn't have a simple base class `A`, but a bunch of them. You may think that your question is super clear, but believe me, it is not. Anyways, it is up to you. If you don't give enough information, it is likely that you won't get good answers. – geza Mar 13 '19 at 12:00
  • @geza: Assuming the model is the only thing I can show, can you tell me what is the reasoning for the name lookup to stick to the base class, instead of returning to the derived class once it looked up `g()`? – tmaric Mar 13 '19 at 12:03
  • @tmaric: this is how C++ works. If you want the "lookup" (note, your terminology is wrong, lookup is a different thing in C++, it is a compile-time thing) done differently, put `virtual` there. In common implementations, in current CPUs, the difference between virtual and non-virtual calls is very small (it is just a function call from a table, indexed by a constant integer. No string operations are done, which you may seem to think). – geza Mar 13 '19 at 12:04
  • @geza: "this is how C++ works" doesn't answer my question. Nowhere did I imply that string operations are done. Yes, it's called member name lookup: The C++ standard, Section 13.2 "Member name *lookup*". – tmaric Mar 13 '19 at 12:46
  • @tmaric: "resolve the function names at runtime". One can easily think that this some string related thing. Like calling a function from .so/.dll. And about lookup, you're still wrong. When you are in `A::g()`, the call `f()` will find `A::f()`. That's name lookup. No matter whether `f` is virtual or not. If `f` is virtual, then `B::f` will override it, and at run-time, `B::f` will be called (If `this` points to `B`) instead of `A::f`. This mechanism is not lookup. Btw., if C++ worked the way you say, what would be the difference between virtual and non-virtual functions? – geza Mar 13 '19 at 13:12
  • How does calling `B b; b.g()` not involve name lookup? `g()` is inherited from `A` and needs to be found. Regarding virtual functions: the difference would be the same as it is now - access of a derived type via a pointer to the base. It is quite clear when this happens and when the types are known at compile time. I'm just simply asking: why isn't `B::f()` used for this specific example, I still don't see a reason to force `A::f()` just because `using A::g()` is in `B`. The base is overriding a derived function in this case, that is visible and more specialized in the object hierarchy. – tmaric Mar 13 '19 at 13:18
  • Of course it involves a lookup. But the mechanism you want from C++ is not called lookup. Anyways, let's drop this subject. :) You seem to misunderstand something. The behavior you see has nothing to do with `using A::g`. When you call `b.g()`, `A::g()` will be called (even if you use `using A::g` in `B`). At that point in `A::g()`, the name `f` will find `A::f`. And as `A::f` is not virtual, `A::f` will be called. – geza Mar 13 '19 at 13:24
  • @geza: OK, thanks a lot for the help! :) I will dig around, maybe I find an example where doing: "find g() in A, go back to B, find f() in B, use it" would break something else somewhere. – tmaric Mar 13 '19 at 13:28

1 Answers1

5

This is a standard task for the CRTP. The base class needs to know what the static type of the object is, and then it just casts itself to that.

template<typename Derived>
struct A
{
    void g() const
    {
        cout << static_cast<Derived const*>(this)->f() << endl;
    }
};

struct B : A<B>
{
    using A::g; 
    double f() const { return 1; };
};

Also, responding to a comment you wrote, (which is maybe your real question?),

can you tell me what is the reasoning for the name lookup to stick to the base class, instead of returning to the derived class once it looked up g()?

Because classes are intended to be used for object-oriented programming, not for code reuse. The programmer of A needs to be able to understand what their code is doing, which means subclasses shouldn't be able to arbitrarily override functionality from the base class. That's what virtual is, really: A giving its subclasses permission to override that specific member. Anything that A hasn't opted-in to that for, they should be able to rely on.

Consider in your example: What if the author of B later added an integer member which happened to be called endl? Should that break A? Should B have to care about all the private member names of A? And if the author of A wants to add a member variable, should they be able to do so in a way that doesn't potentially break some subclass? (The answers are "no", "no", and "yes".)

Sneftel
  • 40,271
  • 12
  • 71
  • 104
  • Thank you for answering the other part as well! I can turn your question around, maybe it helps to show my perspective. By implementing B, I am specializing A in a hierarchy (that is also the case when `virtual` is used). I am then surprised, that when I use `B b; b.g();`, the class `A` *arbitrarily* overrides `f()` ,simply because it shares `g()` with its derived classes. :) Why is this direction OK in the language, and the other one isn't? I would expect that the *most specialized* (down in the hierarchy) object is used. – tmaric Mar 13 '19 at 12:55
  • Also, if `endl` is added to `B`, how can it break `A` under these rules, when `A a; A.g();` is called? Under these rules, everything is first found in `A`. For `A`, there is no base, so there is a compile error because the name lookup didn't find something in this case. Otherwise, its base is searched for `endl`, `endl` is found and resolved in `A` and not the base. – tmaric Mar 13 '19 at 12:56
  • "Should B have to care about all the private member names of A?": It can't and doesn't if they are private. – tmaric Mar 13 '19 at 13:00