如何让GCC/Clang将结构体首个字段通过CPU寄存器返回?
我刚好碰到过类似的问题,针对x86架构下GCC/Clang的这个返回值优化痛点,有几个实用的方案能满足你的需求——既让code字段始终通过寄存器返回,又保留C++17结构化绑定的便捷性:
1. 直接改用std::tuple(最省心)
GCC和Clang对std::tuple<int, T>的返回优化逻辑刚好贴合你的需求:第一个标量成员(也就是你的code)会自动通过寄存器返回,第二个成员哪怕是非POD类型(比如std::string),也会直接构造到调用方的栈空间。而且tuple天然支持C++17的结构化绑定,几乎不需要修改原有代码的调用逻辑:
#include <tuple> #include <string> std::tuple<int, int> FuncInt(bool const flag) { return {flag ? 0 : 1, 42}; } std::tuple<int, std::string> FuncString(bool const flag) { return {flag ? 0 : 1, "DEADBEEF"}; } auto UseInt(bool const flag) { if (auto [code, val] = FuncInt(flag); !code) { return val; } return 0; // 补充默认返回,避免编译警告 } auto UseString(bool const flag) { if (auto [code, val] = FuncString(flag); !code) { return val; } return std::string{}; // 补充默认返回 }
编译后你用gcc -S看汇编就会发现,FuncString把code存在rax寄存器返回,UseString里判断code的时候直接用rax的值,完全不用从栈里读。
2. 让自定义Pair兼容tuple的优化逻辑
如果你想保留自定义Pair结构体的命名和成员访问方式,也可以让它继承std::tuple,再封装一层code()和value()的访问接口,这样既不破坏原有代码的可读性,又能蹭到tuple的返回优化:
#include <tuple> #include <string> #include <utility> // 用于std::integral_constant template <typename T> struct Pair : std::tuple<int, T> { // 继承tuple的所有构造函数 using std::tuple<int, T>::tuple; // 提供原有的成员访问方式 int& code() noexcept { return std::get<0>(*this); } const int& code() const noexcept { return std::get<0>(*this); } T& value() noexcept { return std::get<1>(*this); } const T& value() const noexcept { return std::get<1>(*this); } }; // 为结构化绑定特化tuple相关模板(C++17及以上) namespace std { template <typename T> struct tuple_size<Pair<T>> : integral_constant<size_t, 2> {}; template <typename T> struct tuple_element<0, Pair<T>> { using type = int; }; template <typename T> struct tuple_element<1, Pair<T>> { using type = T; }; } // 原有函数几乎不用改 Pair<int> FuncInt(bool const flag) { return {flag ? 0 : 1, 42}; } Pair<std::string> FuncString(bool const flag) { return {flag ? 0 : 1, "DEADBEEF"}; } // 调用逻辑完全不变 auto UseInt(bool const flag) { if (auto [code, val] = FuncInt(flag); !code) { return val; } return 0; } auto UseString(bool const flag) { if (auto [code, val] = FuncString(flag); !code) { return val; } return std::string{}; }
这样你的代码风格完全没变,但编译器会把Pair当成tuple来优化,code照样走寄存器返回。
3. 手动拆分返回逻辑(编译器扩展友好)
如果你不想依赖标准库的tuple,也可以手动拆分函数逻辑:把code作为返回值(自然走寄存器),value用输出参数传递,再用一个包装函数返回Pair结构体,配合-O2或更高优化级别,编译器会自动把包装逻辑优化掉:
#include <string> template <typename T> struct Pair { int code; T value; }; // 核心逻辑:返回code,value通过输出参数传递 int FuncStringImpl(bool const flag, std::string& out_val) { int code = flag ? 0 : 1; if (!code) { out_val = "DEADBEEF"; } return code; } // 包装函数,支持结构化绑定 Pair<std::string> FuncString(bool const flag) { Pair<std::string> result; result.code = FuncStringImpl(flag, result.value); return result; } // 调用逻辑不变 auto UseString(bool const flag) { if (auto [code, val] = FuncString(flag); !code) { return val; } return std::string{}; }
这个方案的好处是完全自定义,不依赖任何标准库扩展,只要开了优化,GCC和Clang都会把包装函数的冗余代码删掉,最终效果和你想要的int UseString(bool, std::string&)完全一致,同时还能保留结构化绑定的写法。
你可以用gcc -S -O2 your_code.cpp生成汇编代码,或者用objdump -d your_binary反编译,重点看这两点:
FuncString的返回部分是否把code存入rax寄存器;UseString里判断!code的时候,是不是直接用rax的值,而不是从栈内存读取。
内容的提问来源于stack exchange,提问作者Denís

