Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does GCC delete my code on O3 but not on O0?

Tags:

c++

Recently i have been trying to learn about rvalues and perfect forwarding. While playing around with some constructs i came across some peculiar behavior while switching compilers and optimization levels.

Compiling the same code on GCC with no optimization turned on would yield expected results, however turning on any optimization level then would result all of my code being deleted. Compiling the same code on clang with no optimizations also yields expected results. Then turning on optimizations on clang would still yield expected results.

I know this screams undefined behavior but i just cannot figure out what exactly is going wrong and whats causing the discrepancy between the two compilers.

gcc -O0 -std=c++17 -Wall -Wextra

https://godbolt.org/z/5xY1Gz

gcc -O3 -std=c++17 -Wall -Wextra

https://godbolt.org/z/fE3TE5

clang -O0 -std=c++17 -Wall -Wextra

https://godbolt.org/z/W98fh8

clang -O3 -std=c++17 -Wall -Wextra

https://godbolt.org/z/6sEo8j

#include <utility>

// lambda_t is the type of thing we want to call.
// capture_t is the type of a helper object that 
// contains all all parameters meant to be passed to the callable
template< class lambda_t, class capture_t >
struct CallObject {

    lambda_t  m_lambda;
    capture_t m_args;

    typedef decltype( m_args(m_lambda) ) return_t;

    //Construct the CallObject by perfect forwarding which is
    //neccessary as they may these are lambda which will have
    //captured objects and we dont want uneccessary copies
    //while passing these around
    CallObject( lambda_t&& p_lambda, capture_t&& p_args ) :
        m_lambda{ std::forward<lambda_t>(p_lambda) },
        m_args  { std::forward<capture_t>(p_args) }
    {

    }

    //Applies the arguments captured in m_args to the thing
    //we actually want to call
    return_t invoke() {
        return m_args(m_lambda);
    }

    //Deleting special members for testing purposes
    CallObject() = delete;
    CallObject( const CallObject& ) = delete;
    CallObject( CallObject&& ) = delete;
    CallObject& operator=( const CallObject& ) = delete;
    CallObject& operator=( CallObject&& ) = delete;
};

//Factory helper function that is needed to create a helper
//object that contains all the paremeters required for the 
//callable. Aswell as for helping to properly templatize
//the CallObject
template< class lambda_t, class ... Tn >
auto Factory( lambda_t&& p_lambda, Tn&& ... p_argn ){

    //Using a lambda as helper object to contain all the required paramters for the callable
    //This conviently allows for storing value, references and so on
    auto x = [&p_argn...]( lambda_t& pp_lambda ) mutable -> decltype(auto) {

        return pp_lambda( std::forward<decltype(p_argn)>(p_argn) ... );
    };

    typedef decltype(x) xt;
    //explicit templetization is not needed in this case but
    //for the sake of readability it needed here since we then
    //need to forward the lambda that captures the arguments
    return CallObject< lambda_t, xt >( std::forward<lambda_t>(p_lambda), std::forward<xt>(x) );
}

int main(){

    auto xx = Factory( []( int a, int b ){

        return a+b;

    }, 10, 3 );

    int q = xx.invoke();

    return q;
}
like image 779
mradek92 Avatar asked Jan 25 '23 20:01

mradek92


2 Answers

If something like this happens, it's ususally because you have undefined behavior somewhere in your program. The compiler did detect this and when optimizing aggressively will throw away your entire program as a result.

In your concrete example, you already get a hint that something is not quite right in the form of a compiler warning:

<source>: In function 'int main()':
<source>:45:18: warning: '<anonymous>' is used uninitialized [-Wuninitialized]
   45 |         return a+b;
      |                  ^

How could this happen? What could lead to b being uninitialized at this point?

Since b is a function parameter at this point, the problem must lie with the caller of that lambda. Examining the call site we notice something fishy:

auto x = [&p_argn...]( lambda_t& pp_lambda ) mutable -> decltype(auto) {
    return pp_lambda( std::forward<decltype(p_argn)>(p_argn) ... );
};

The argument bound to b is passed as a parameter pack p_argn. But notice the lifetime of that parameter pack: It's captured by reference! So there is no perfect forwarding going on here, despite the fact that you wrote std::forward in the lambda body, because you capture by reference in the lambda, and the lambda does not "see" what happens outside its body in the surrounding function. You get the same lifetime problem with a here too, but for some reason, the compiler chooses not to complain about that one. That's undefined behavior for you, there's no guarantee you'll get a warning for it. The quickest way to fix this is to just capture the arguments by value. You can retain the perfect forwarding property by using a named capture, with the somewhat peculiar syntax:

auto x = [...p_argn = std::forward<decltype(p_argn)>(p_argn)]( lambda_t& pp_lambda ) mutable -> decltype(auto) {
    return pp_lambda(std::move(p_argn)... );
};

Be sure you understand what is actually being stored where in this case, maybe even draw a picture. It is vital to be able to know exactly where the individual objects live when writing code like this, otherwise it's very easy to write lifetime bugs like this.

like image 118
ComicSansMS Avatar answered Feb 03 '23 13:02

ComicSansMS


Why does GCC delete my code on O3

Because GCC is very smart, figures out that your program doesn't depend on any runtime input, and thus optimises it to constant output at compile time.

just cannot figure out what exactly is going wrong and whats causing the discrepancy between the two compilers.

The behaviour of the program is undefined. There is no reason to expect there to not be discrepancy between compilers, or any particular behaviour.

The behaviour of the program is undefined.

But why?

Here:

auto xx = Factory(the_lambda, 10, 3);

You pass literals to the function, which are prvalues.

auto Factory( lambda_t&& p_lambda, Tn&& ... p_argn )

The function accepts them by reference. Therefore temporary objects are created, whose lifetime extends until the end of the full expression (which is longer than lifetime of the argument references, so the lifetime of the temporaries are not extended).

auto x = [&p_argn...]( //...

The referred temporaries are stored in a lambda... by reference. At no point is there an integer stored in a lambda.

When you later call the lambda, those temporary objects that were referred no longer exist. Those non-existing objects are accessed outside their lifetime, and the behaviour of the program is undefined.


Mistakes like this are the reason why std::thread, std::bind and similar which bind arguments always store a value rather than a reference.

like image 23
eerorika Avatar answered Feb 03 '23 14:02

eerorika