C++ Logo

std-proposals

Advanced search

[std-proposals] Not quite a template virtual function

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Tue, 1 Oct 2024 11:19:30 +0100
The following program compiles just fine:

    class Monkey {
    public:
        double Do(int const arg) { return arg * 3.0; }
        int Do(char, char, char) { return 6; }
    };

    class BlueMonkey : public Monkey {
    public:
        decltype(auto) Act(auto&&... args)
        {
            return this->Do( static_cast<decltype(args)>(args)... );
        }
    };

    int main(void)
    {
        BlueMonkey bm;
        bm.Act(1,2,3);
    }


But now let's make a small alteration. Let's make the all the methods virtual:

    class Monkey {
    public:
        virtual double Do(int const arg) { return arg * 3.0; }
        virtual int Do(char, char, char) { return 6; }
    };

    class BlueMonkey : public Monkey {
    public:
        virtual decltype(auto) Act(auto&&... args)
        {
            return this->Do( static_cast<decltype(args)>(args)... );
        }
    };

    int main(void)
    {
        BlueMonkey bm;
        bm.Act(1,2,3);
    }

So now we get the following compiler error:

      error: virtual function cannot have deduced return type
           | virtual decltype(auto) Act(auto&&... args)

So let's replace the 'Act' method's return type with 'int' as follows:

        virtual int Act(auto&&... args)
        {
            return this->Do( static_cast<decltype(args)>(args)... );
        }

We re-compile and the error we get is:

      error: implicit templates may not be 'virtual'
           | virtual int Act(auto&&... args)

Now let's just have a quick rundown on why virtual template functions
should be forbidden. Let's say we have the following simple class:

    struct Simple {
        template<typename T>
        virtual bool Do(T &&arg)
        {
            return !static_cast<bool>( std::forward<T>(arg) );
        }
     };

If we put this class in a header file, and then include it in two
separate source files "a.cpp" and "b.cpp", and if one of those source
files instantiates the template function differently than the other,
then the Vtable for 'Simple' in "a.cpp" will be different from the
Vtable for 'Simple' in "b.cpp". This is the reason why we can't have
template virtual functions -- the same class can't have different
Vtables in different translation units.

But let's go back to my original example though:

    class BlueMonkey : public Monkey {
    public:
        virtual decltype(auto) Act(auto&&... args)
        {
            return this->Do( static_cast<decltype(args)>(args)... );
        }
    };

Of course this 'Act' method _looks_ _like_ a template function because
it has 'auto' in its parameter list, but really since 'args' is passed
directly to the 'Do' method, we know that 'Do' really has only two
possible signatures:

        double Do(int);
        int Do(char, char, char);

which means that 'Act' can really only have two possible signatures:

        double Act(int);
        int Act(char, char, char);

Conceivably, when the compiler encounters the 'Act' method, it could
make exactly two additions to the Vtable, one for the implementation
that uses "Do(int)" and one for the implementation that uses
"Do(char,char,char)", and therefore the two seprate source files
"a.cpp" and "b.cpp" will have identical Vtables for the 'BlueMonkey'
class.

So would it be possible to relax the rules on virtual template
functions, if the inline body of the function contains a finite number
of possibilities for how the virtual template function can be
instantiated?

Of course this could get really complicated if the body of the
function contains "if constexpr( requires { ... } )" and the like, but
for the time being I'm just talking about very simply function bodies
-- perhaps even limiting it to function bodies that only contain one
return statement which invokes a non-template function?

I came up with this idea yesterday when I was doing some really
intricate debugging. An SDK library had a function called
'GetPowerManagement' which returned a pointer to an 'IPowerManagement'
object, and so I wrote a "man in the middle" library that intercepted
the call to "GetPowerManagement". Before returning the pointer from
"GetPowerManagement", I intercepted each of the method calls to add
logging, something like this:

    class IPowerManagement {
    public:
        virtual bool SetGrade(unsigned) = 0;
        virtual bool SetLevel(unsigned) = 0;
    };

    class Interceptor_PowMan : public IPowerManagement {
        IPowerManagement *const pPM;
    public:
        Interceptor_PowMan(IPowerManagement *const arg) : pPM(arg) {}

        virtual bool SetGrade(unsigned const arg) override
        {
            // Do logging
            return this->pPM->SetGrade(arg);
        }

        virtual bool SetLevel(unsigned const arg) override
        {
            // Do logging
            return this->pPM->SetLevel(arg);
        }
    };

    IPowerManagement *GetPowerManagement(void)
    {
        // This is the man in the middle function

        // Invoke the original function:
        auto const retval = Original_GetPowerManagement();
        // and now wrap it up:
        return new Interceptor_PowMan(retval);
    }

Look at how I had to write out each individual method, SetGrade and
SetLevel (there was more like a dozen of them to write out). It would
have been handy, if instead of writing:

        virtual bool SetLevel(unsigned const arg) override
        {
            // Do logging
            return this->pPM->SetLevel(arg);
        }

if I had been able to write:

        virtual decltype(auto) SetLevel(auto &&args) override
        {
            // Do logging
            return this->pPM->SetLevel( static_cast<decltype(args)>(args)... );
        }

In fact, it would have been even easier if I had been able to do something like:

        virtual decltype(auto) SetLevel,SetGrade(auto &&args) override
        {
            // Do logging
            return this->pPM-> __func __ (
static_cast<decltype(args)>(args)... );
        }

But anyway this is just one use case.

Maybe it would be helpful to allow template virtual functions in cases
where the number of possible instantiations is very small and very
easy to deduce.

Received on 2024-10-01 10:19:43