首页 > 解决方案 > 为什么 GCC 会删除我在 O3 上的代码,而不是在 O0 上?

问题描述

最近我一直在尝试学习右值和完美转发。在玩弄一些结构时,我在切换编译器和优化级别时遇到了一些特殊的行为。

在没有打开优化的情况下在 GCC 上编译相同的代码会产生预期的结果,但是打开任何优化级别都会导致我的所有代码都被删除。在没有优化的情况下在 clang 上编译相同的代码也会产生预期的结果。然后在 clang 上打开优化仍然会产生预期的结果。

我知道这会引起未定义的行为,但我只是无法弄清楚到底出了什么问题以及是什么导致了两个编译器之间的差异。

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;
}

标签: c++

解决方案


如果发生这样的事情,通常是因为您在程序的某个地方有未定义的行为。编译器确实检测到了这一点,并且在积极优化时会因此丢弃整个程序。

在您的具体示例中,您已经以编译器警告的形式得到了一些不太正确的提示:

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

这怎么可能发生?什么可能导致b此时未初始化?

由于b此时是一个函数参数,因此问题必须出在该 lambda的调用者身上。检查调用站点,我们注意到一些可疑的地方:

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

绑定到b的参数作为参数包传递p_argn。但请注意该参数包的生命周期:它是通过引用捕获的!因此,尽管您在 lambda 主体中编写了这一事实,但这里并没有完美的转发std::forward,因为您在 lambda 中通过引用捕获,而 lambda 不会“看到”在其主体之外在周围函数中发生的事情。你在a这里也会遇到同样的生命周期问题,但由于某种原因,编译器选择不抱怨那个问题。这对您来说是未定义的行为,无法保证您会收到警告。解决此问题的最快方法是仅按值捕获参数。您可以使用命名捕获保留完美的转发属性,语法有些特殊:

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)... );
};

确保您了解在这种情况下实际存储的内容,甚至可以绘制图片。在编写这样的代码时,能够准确地知道各个对象所在的位置至关重要,否则很容易编写这样的终身错误。


推荐阅读