inline empty polymorphic functions

This is a discussion on inline empty polymorphic functions within the C++ Programming forums, part of the General Programming Boards category; Suppose condition is true. Can I ever expect a compiler to omit the bolded virtual function call? Code: #include <iostream> ...

  1. #1
    Kiss the monkey. CodeMonkey's Avatar
    Join Date
    Sep 2001
    Posts
    891

    inline empty polymorphic functions

    Suppose condition is true. Can I ever expect a compiler to omit the bolded virtual function call?
    Code:
    #include <iostream>
    
    namespace scratch
    {
        
        class foo
        {
          public:
            foo(method & m_) : m(m_) {}
            void operator()()
            {
                std::cout << "Functor called." << std::endl;
                m->call();
            }
          private:
            method & m;
        };
        
        
        struct method { virtual void call() = 0; };
        
        class trivial : public method
        {
          public:
            void call() {}
        };
        
        class nontrivial : public method
        {
          public:
            void call()
            {
                //Juicy with code
            }
        };
    
    }//namespace scratch
    
    int main()
    {
        using namespace scratch;
    
        method * m = (condition) ? new trivial: new nontrivial;
        foo functor(*m);
        functor();
        delete m;
    }
    As was suggested on a previous thread, I'm going to implement a customize-your-own-function sort of class by having the operator() call a bunch of function objects from reference, a few of which might have been chosen to do nothing. What is the overhead involved in calling a virtual function that does nothing? How can I find out in the future to avoid asking such questions? What is the answer to life, the universe, and the meaning of everything?

    *edit* I can probably spare a few cycles per call, but any more will add up quickly. The operator() will have a tremendous loop inside of it.
    "If you tell the truth, you don't have to remember anything"
    -Mark Twain

  2. #2
    Registered User
    Join Date
    Jun 2005
    Posts
    5,857
    Your code, as shown, will not compile. Firstly, within the definition of class foo, method is (at most) a forward declaration, which means you cannot call its member functions. Second, even if you ensure a definition of method is visible within foo::operator(), m is of type reference to method, a type that does not support an operator->(), so the statement m->foo() is invalid.

    Assuming you fix all those problems, the only way to stop a virtual function being called is ..... don't call it. The compiler will not magically recognise that the function body is empty, and decide not to call it. If your code calls a virtual function through a pointer or reference to base, the overhead of performing the call will always be incurred even if the result is calling a function that does nothing.

  3. #3
    Kiss the monkey. CodeMonkey's Avatar
    Join Date
    Sep 2001
    Posts
    891
    Right, my mistake. But you answered my question flatly anyhow. Thanks.
    "If you tell the truth, you don't have to remember anything"
    -Mark Twain

  4. #4
    Kernel hacker
    Join Date
    Jul 2007
    Location
    Farncombe, Surrey, England
    Posts
    15,677
    As an aside, it won't make much difference if you add a variable "emptyfunction" and write soemthing like "if (!emptyfunction) call()" - the overhead will probably be almost as bad as the function call itself, and you are going to have some of that overhead even when you are actually using the function.

    --
    Mats
    Compilers can produce warnings - make the compiler programmers happy: Use them!
    Please don't PM me for help - and no, I don't do help over instant messengers.

  5. #5
    Registered User
    Join Date
    Jun 2005
    Posts
    5,857
    Quote Originally Posted by matsp View Post
    As an aside, it won't make much difference if you add a variable "emptyfunction" and write soemthing like "if (!emptyfunction) call()" - the overhead will probably be almost as bad as the function call itself, and you are going to have some of that overhead even when you are actually using the function.
    Not so sure about the "overhead", but this would introduce a maintenance concern: the programmer is required to remember to set the value of emptyfunction depending on content of the function. That is an opportunity to get things out of sync, potentially with disastrous results.

  6. #6
    Kernel hacker
    Join Date
    Jul 2007
    Location
    Farncombe, Surrey, England
    Posts
    15,677
    Quote Originally Posted by grumpy View Post
    Not so sure about the "overhead", but this would introduce a maintenance concern: the programmer is required to remember to set the value of emptyfunction depending on content of the function. That is an opportunity to get things out of sync, potentially with disastrous results.
    That is of course an very good point. I was thinking, be a property of the class (so set in the constructor) rather than something done for each instance of the functor. But it is still "more to know about".

    Since it ALSO isn't gaining anything (assuming modern processor and optimized code), I'd say both arguments combined says "bad idea".

    --
    Mats
    Compilers can produce warnings - make the compiler programmers happy: Use them!
    Please don't PM me for help - and no, I don't do help over instant messengers.

  7. #7
    Cat without Hat CornedBee's Avatar
    Join Date
    Apr 2003
    Posts
    8,892
    Let's see. Here are some transformations that a compiler could do (and that, in principle, some compilers actually do):

    1) Conditional operator to if, for ease of explanation. This is not something the compiler actually does, just something that is a natural consequence of the intermediate form most compilers use:
    Code:
    int main()
    {
        using namespace scratch;
    
        method * m;
        if(condition)
            m = new trivial;
        else
            m = new nontrivial;
        foo functor(*m);
        functor();
        delete m;
    }
    2) Lifting of code into the if branches, if the compiler thinks keeping the cases separate could lead to better code.
    Code:
    int main()
    {
        using namespace scratch;
    
        method * m;
        if(condition) {
            m = new trivial;
            foo functor(*m);
            functor();
            delete m;
        } else {
            m = new nontrivial;
            foo functor(*m);
            functor();
            delete m;
        }
    }
    3) Complete inlining of the foo object. This is where we leave real C++ syntax.
    Code:
    int main()
    {
        using namespace scratch;
    
        method * m;
        if(condition) {
            m = new trivial;
            // Inline storage
            { method& $1_m } functor;
            // Inline constructor
            $1_m = *m;
            // Inline operator()
            std::cout << "Functor called." << std::endl;
            $1_m.call();
            delete m;
        } else {
            m = new nontrivial;
            // Inline storage
            { method& $2_m } functor;
            // Inline constructor
            $2_m = *m;
            // Inline operator()
            std::cout << "Functor called." << std::endl;
            $2_m.call();
            delete m;
        }
    }
    4) Eliminate redundant aliases, and lift the declaration of m into the branches, with updated type information.
    Code:
    int main()
    {
        using namespace scratch;
    
        if(condition) {
            trivial * m = new trivial;
            std::cout << "Functor called." << std::endl;
            m->call();
            delete m;
        } else {
            nontrivial * m = new nontrivial;
            std::cout << "Functor called." << std::endl;
            m->call();
            delete m;
        }
    }
    5) Inline statically proven virtual calls.
    Code:
    int main()
    {
        using namespace scratch;
    
        if(condition) {
            trivial * m = new trivial;
            std::cout << "Functor called." << std::endl;
            delete m;
        } else {
            nontrivial * m = new nontrivial;
            std::cout << "Functor called." << std::endl;
            // Juicy code
            delete m;
        }
    }
    6) There you go, the virtual call is gone. The compiler could now go on to inline the constructors in the new expressions and find that trivial's constructor doesn't do anything. It then might (but won't, because no compiler treats allocation-deallocation as side-effect-free) completely omit the allocation of the trivial object. It could, if it proves that nontrivial's constructor has no side effects, reorder the new expression below the cout, lift the common expression of the branches out, and then completely remove the now empty true branch. The end result would then be:
    Code:
    int main()
    {
        using namespace scratch;
    
        std::cout << "Functor called." << std::endl;
        if(!condition) {
            nontrivial * m = new nontrivial;
            // Juicy code - or just the call if it's too long
            delete m;
        }
    }


    This is theory. What a compiler really does in practice is a QoI issue. However, given the assumptions that allocation does nothing but allocate and order of allocations is unimportant (neither of which holds, but let's assume a really nice system), the program is a valid transformation of your original one. Even without the assumption, you can transform to the point where the virtual call is gone.
    All the buzzt!
    CornedBee

    "There is not now, nor has there ever been, nor will there ever be, any programming language in which it is the least bit difficult to write bad code."
    - Flon's Law

  8. #8
    Kernel hacker
    Join Date
    Jul 2007
    Location
    Farncombe, Surrey, England
    Posts
    15,677
    Yes, I agree with CornedBee.

    However, if the actual implemented code is a bit more complex and involves creating the functor in one place and using it in a completely different place, the compiler will not necessarily be able to track the actual conditions and resulting code.

    --
    Mats
    Compilers can produce warnings - make the compiler programmers happy: Use them!
    Please don't PM me for help - and no, I don't do help over instant messengers.

  9. #9
    Kiss the monkey. CodeMonkey's Avatar
    Join Date
    Sep 2001
    Posts
    891
    Thanks, guys. CornedBee really broke it down.
    "If you tell the truth, you don't have to remember anything"
    -Mark Twain

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Help with binary file c++
    By lucky_mutani in forum C++ Programming
    Replies: 4
    Last Post: 06-05-2009, 09:24 AM
  2. conditional breakpoints and inline functions
    By Mario F. in forum C++ Programming
    Replies: 2
    Last Post: 08-10-2006, 08:30 PM
  3. Inline functions and passing by value
    By gentinex in forum C++ Programming
    Replies: 8
    Last Post: 06-07-2006, 06:53 AM
  4. bit shifting
    By Nor in forum C++ Programming
    Replies: 9
    Last Post: 08-08-2003, 11:55 AM
  5. Replies: 5
    Last Post: 09-17-2001, 06:18 AM

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21