0

This is a followup question of Static polymorphism with final or templates?. So it seems that the best solution for static-only polymorphism is to use CRTP. If you want runtime polymorphism, virtual functions are a good solution.

I find that not very elegant because the problems are actually very similar (and maybe you want to change the behavior at some point) but the code is actually very different. The code would in my opinion be more expressive if the solutions would be very similar and only differ at a single spot.

So I would like to know if there is a way to get static-only polymorphism with virtual functions. That might be something like an attribute or some construction to not allow pointers to the abstract base class. Is there such a feature and if not, am I missing something why such a feature should not exist? Are static and runtime polymorphism actually more different than such a feature would suggest?

EDIT: To make the question and the usecase a bit clearer, here is some example, that I would like to write:

[[abstract]] class Base {
public:
  void bar() { /* do something using foo() */ }
private:
  virtual void foo() = 0;
};

class Derived1 : public Base {
public:
  Derived1();
private:
  void foo() override { /* do something */};
};

class Derived2 : pulic Base {
public:
  Derived2();
private:
  void foo() override { /* do something with data */ }
  int data;
};
  

where the non-existing attribute [[abstract]] means that there cannot exist an instance of the class Bass, even not by a pointer. This would clearly express static polymorphism and the compiler could optimize away virtual calls because they do not exist. Also a virtual destructor would not be necessary.

EDIT 2: The goal is to provide an abstract interface that can be slightly modified in further derived classes and has the same options for extending as an abstract class. So the main implementation is still in Base and the specific implementation of the virtual functions is in Derived.

Henk
  • 826
  • 3
  • 14
  • Since virtual functions are for runtime-only polymorphism, I'm not sure what you mean by trying to "get static-only polymorphism with virtual functions." It's like trying to go swimming in dry water because you don't want to get wet. – Eljay Jan 19 '21 at 15:46
  • _What_ code is very different? The site of a statically-or-dynamically-dispatched method call on an object? You can absolutely write a function template that generates both virtual and non-virtual calls depending on the type parameters. Is that what you want to do? The question is very unclear. – Useless Jan 19 '21 at 15:48
  • I want static-only polyphormism that code-wise looks pretty similar to virtual functions for runtime polymorphism, such that it can for example be easily exchanged to support runtime polymorphism. I do not like that CRTP for example contains a lot of boilerplate just to express that you want static polyphormism instead of runtime polyphormism. – Henk Jan 19 '21 at 15:54
  • @Henk An actual use-case would make your question more clear. Just calling it "*static polymorphism*" can mean different things in different contexts. It could be argued that CRTP alone does not provide any kind of polymorphism, since there is no common base or interface contract, but rather something like code injection into any number of disparate classes. – dxiv Jan 19 '21 at 16:02
  • Right, so is it the declaration & definition of the polymorphic types you want to look similar? Or the code that _uses_ them? – Useless Jan 19 '21 at 16:03
  • You know the optimizer is _already_ allowed to devirtualize calls when it knows the static type - and if you just don't use base-class references, you probably _always_ have the static type. What problem are you trying to solve, that uses (possibly) virtual dispatch without base-class references? And how is that still polymorphism? – Useless Jan 19 '21 at 16:33
  • The point is to get the devirtualization guaranteed and expressing the intent of the virtual approach, that actually is not virtual. – Henk Jan 19 '21 at 16:36
  • So your question is how to write something that looks as similar as possible to runtime polymorphism (`virtual`) at the point of declaration and definition, but can only be used as static polymorphism. But you don't need CRTP or a base class at all, you can just use duck-typing. Is this worse because it _doesn't_ have a keyword like `virtual`? – Useless Jan 19 '21 at 16:40
  • @Henk Not sure I follow the example you edited in. `void foo() override;` This would "*override*" what exactly, since `Base` is not a parent of `Derived`? You can't override every single function named `foo` in all unrelated classes, or you lose any semblance of encapsulation. – dxiv Jan 19 '21 at 16:49
  • So is the goal of this to get a compile error if you forget to override part of Base? Or to maybe later change it to runtime virtual dispatch? Or something else? Do you really just want it to _look_ like a virtual function for aesthetic reasons? – Useless Jan 19 '21 at 17:00
  • @Henk "*there cannot exist an instance of the class Bass, even not by a pointer*" If by that you mean `void bar(Base *p) { p->foo(); }` would not be allowed, then there is no polymorphism involving `Base`, and I don't see what the difference would be vs. a plain non-virtual `foo`. – dxiv Jan 19 '21 at 17:01
  • Well, it would not be possible to have a general implemenation in the base class that uses ```foo()``` for example. I clarified the goal. – Henk Jan 19 '21 at 17:15
  • So what you're saying is you want to use CRTP to implement something, _and_ you want static polymorphism in some other context you haven't shown, _and_ you think those things are connected. But the CRTP is an implementation detail, that just happens to also be useable with normal duck-typed static polymorphism. – Useless Jan 19 '21 at 17:36
  • Ah, no - you _only_ care about the common-implementation-in-the-base-class bit? So not about general polymorphism at all - is that right? You just want exactly the CRTP use case with nicer syntax? – Useless Jan 19 '21 at 22:39
  • Yeah, I think that this describes my problem much better. Sorry for the confusion! – Henk Jan 19 '21 at 22:40

3 Answers3

1

You are more likely to get something the other way around, runtime polymorphism that looks like static polymorphism, or that looks like something completely different.

The metaclass proposals floated for post-reflection C++ (maybe ) look powerful enough to do stuff like:

Interface IBob {
  void foo();
};

and

Implementation<Dispatch::Static> BobImpl:IBob {
  void foo() {}
};
Implementation<Dispatch::Dynamic> BobImpl:IBob {
  void foo() {}
};

to do roughly what you are asking. (Syntax is ridiculously far from final in the metaclass proposal(s); the expressive power is clearly there to do the above, however).

The dynamic case would set up vtables and the like (possibly not the standard C++ vtables however), and in the static case BobImpl would be unrelated to the type bob.

Of course, at that point, I expect there to be so many new ways to express polymorphism in C++ that "I want my CRTP to be written like a virtual function table C++ object" to be a bit like seeing atomic power technology coming over the horizon, and being excited that it could replace the coal burner on your steam-train.

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

So it seems that the best solution for static-only polymorphism is to use CRTP. If you want runtime polymorphism, virtual functions are a good solution.

In your linked question, you used CRTP to enforce an interface:

template <typename TData>
struct Base {
   void foo() {
       static_cast<TData*>(this)->doFoo();
   }

This uses static polymorphism, but it's a very specific use. It does nothing at all but refuse to compile if your derived class doesn't have a suitable doFoo method. So I don't know how you reached your conclusion.

I find that not very elegant because the problems are actually very similar (and maybe you want to change the behavior at some point) but the code is actually very different. The code would in my opinion be more expressive if the solutions would be very similar and only differ at a single spot.

You lost me. Runtime polymorphism affects two things in C++:

  1. declaration, since you must have a base class, the virtual keyword, and optionally override and final

  2. use, when the call site uses virtual dispatch to find the correct method implementation

    (although, as discussed, this may be optimized out when the static type is known).

Note also that although polymorphism is often discussed as being about relationships between objects, in C++ we're really only talking about method dispatch.

Static polymorphism only affects the call site. The fact that your other question used CRTP doesn't mean that is the only way of using static polymorphism.

If I write a template function

template <typename T>
void foo_it(T&& t) { t.foo(); }

then that uses static polymorphism. It will work for any T with a suitable foo method, whether it derived from Base<T> or not. It will even work for a T which overrides a virtual foo() from some other base class. This is duck typing.

Since it's unclear what you hope your [[abstract]] Base to achieve, I can only advise you to just write

class Derived {
public:
  void foo();
};

and pass it around to function templates that expect some type implementing foo().


As a follow-up to your edit

So the main implementation is still in Base and the specific implementation of the virtual functions is in Derived

It's perfectly fine to do this using CRTP. That is an implementation detail which happens to use static polymorphism, not a virtual-like hierarchy.

For example

template <typename Derived>
struct Template {
  Derived* virt() { return static_cast<Derived*>(this); }

  int foo(int i) {
    return i + virt->detail(i) + virt->extra();
  }
};

struct A: public Template<A> {
  int detail(int i) { return i*i; }
  int extra() { return 17; }
};

struct B: public Template<B> {
  int detail(int i) { return i % 23; }
  int extra() { return -42; }
};

creates two independent types A and B, which provide the same interface int foo(int), and happen to share some code as an implementation detail.

It doesn't create a hierarchy. If you write a function template that takes some object of type T and calls the method int T::foo(int) on it, this will work. That is static polymorphism. It doesn't require a shared base class.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • I tried to make clearer what I want to achieve. The ```Base``` class should provide an interface that can be used statically. – Henk Jan 19 '21 at 17:17
  • And my point is you don't _need_ a base-class interface to use anything statically. You can use CRTP to build your general implementation, but that's a detail. `CRTP` and `CRTP` don't need a common base class to both be used in static polymorphism. They both provide the same duck type, and that's good enough. – Useless Jan 19 '21 at 17:34
  • Yeah sorry, I guess my objective was not very well-stated. I basically want static polyphormism with the same features as the implemantation of a classical abstract class (and not be totally different like CRTP). I hope my edit of the question now makes that clearer. – Henk Jan 19 '21 at 17:44
  • When we discuss runtime polymorphism, it means we can have a base-class object reference which is _really_ a derived-type object. The use of this specifically to have implementation in the base customized by virtual hooks in the derived type is an edge case. It's not the main or general use of polymorphism. You can't just say "polymorphism" and expect people to think immediately of that. – Useless Jan 19 '21 at 22:36
0

Assuming the question is how to enforce at compile time that derived classes

  • implement a base "abstract interface"
  • without polymorphism
  • keeping the implementation non-public, yet accessible to base class members

the following could be one way to do it.

#include <type_traits>
#include <iostream>
using std::cout;
using std::endl;

template<class Impl> class Base {
protected:
  void foo() {
    Bridge::virtual_foo(static_cast<Impl&>(*this));
  }

  struct Bridge : public Impl {
    static void virtual_foo(Impl &that) {
      static constexpr void (Impl::*fn)() = &Bridge::foo;
      (that.*fn)();
    }
    static_assert(std::is_same<void (Impl::*)(), decltype(&Bridge::foo)>::value, "foo not implemented");
  };

public:
  void bar() {
    cout << "begin Base::bar" << endl;
    foo();
    cout << "end Base::bar" << endl << endl;
  }
};

class Good : public Base<Good> {
protected:
  void foo() {
    cout << "in Good::foo" << endl;
  }
};

class Bad : public Base<Bad> {
};

int main() {
  Good().bar();
// Bad().bar(); // static_assert: 'foo' not implemented
}

Output:

begin Base::bar
in Good::foo
end Base::bar
dxiv
  • 16,984
  • 2
  • 27
  • 49