C++ 在构造时组合对象时避免数据的多个副本,而无需动态分配
我们有由类表示的分层消息。它们用于通过序列化/反序列化在线程和组件之间发送消息。在我们的用例中,我们使用C++ 在构造时组合对象时避免数据的多个副本,而无需动态分配,c++,constructor,embedded,c++17,C++,Constructor,Embedded,C++17,我们有由类表示的分层消息。它们用于通过序列化/反序列化在线程和组件之间发送消息。在我们的用例中,我们使用std::variant,但为了简化,我们的代码类似于: 类内部{ 公众: 内部(uint8_t*阵列,uint16_t阵列长度){ m_payloadLength=arrayLength;//假设arrayLength始终小于256 memcpy(m_payload.data(),数组,数组长度)); } std::阵列m_有效载荷; uint16_t m_有效负载长度; } 类外部{ 公众
std::variant
,但为了简化,我们的代码类似于:
类内部{
公众:
内部(uint8_t*阵列,uint16_t阵列长度){
m_payloadLength=arrayLength;//假设arrayLength始终小于256
memcpy(m_payload.data(),数组,数组长度));
}
std::阵列m_有效载荷;
uint16_t m_有效负载长度;
}
类外部{
公众:
外部(常量内部和内部):m_内部(内部){};
内m_内;
}
类路由路由器{
公众:
OuterOuter(const-Outer和Outer):m_-Outer(Outer){};
外层m_外层;
}
因此,要创建一个OuterOuter
对象,我们需要执行以下操作
int main(int argc,char**argv){
uint8_t缓冲区[4]={1,2,3,4};
内部(缓冲器,4);
外部(内部);
路由器(外部);
addToThreadQueue(路由器);
}
现在的问题是,我们使用的是嵌入式设备,所以我们不能在malloc和new中使用动态内存。到目前为止,有效负载内容是否会被复制三次?一次用于创建内部,一次用于在外部调用内部的复制构造函数,一次用于在外部路由器中调用外部的复制构造函数?如果是这样,有没有一种方法可以避免在不使用动态内存的情况下进行复制?如果有一种方法可以将我的意图传递给编译器,那么优化是可能的,如果它还没有对它进行优化的话
理想情况下,我们应该避免使用
OuterOuter
类获取所有子类构造参数,因为我们的树非常深,并且我们使用std::variant。在这个例子中,它将是外部路由器(uint8\u t*数组,uint16\u t数组长度)
,外部路由器(uint8\u t*数组,uint16\u t数组长度)
,然后外部路由器将构建内部路由器,现代编译器在优化类层次结构的构造方面做得很好,除了填充连续内存布局之外,这些类层次结构对它们的构造没有任何副作用
例如,gcc将示例编译为一个基本类:
main:
sub rsp, 280
mov eax, 4
mov rdi, rsp
mov WORD PTR [rsp+256], ax
mov DWORD PTR [rsp], 67305985
call addToThreadQueue(OuterOuter const&)
xor eax, eax
add rsp, 280
ret
除此之外,编译器还可以跳过某些场景中的一些副作用。例如,在下面的示例中,gcc通过一个名为“heap elision”的进程完全摆脱了堆分配
显然,您需要重新检查自己的代码库,但通常的重复仍然是:“首先编写易于阅读和维护的代码,并且只有当编译器在这方面做得不好时,您才应该费劲地跳转来优化它。”您可以使用inplacer(请参阅)。您的代码将显示:
Edit:类似的解决方案(但没有inplacer
)--它不能与聚合一起工作,但我打赌在您的情况下这不是问题。是的,有效负载被复制了三次。为了避免这种情况(同时也避免new
和malloc
),只需使用一个指向有效负载内容的指针,而不是将有效负载复制到数组成员变量中。问题是,当我们将对象附加到队列以发送到线程时,它会被复制,因为我们不知道它将在何时被处理。当指针被处理时,指针可能不是有效的。请考虑默认地构造一个<代码>外层< /代码>,然后直接将数据写入该实例的<代码>内部< /代码>。我对您的代码感到困惑。在这段代码中,为什么不使用移动语义?@N0ll\u Boy•没有可移动的资源,因此移动语义与副本没有任何不同。当编译器无法查看INTERNAR的ctor时(或当它有无法优化的副作用时),asm是什么样子的。具体地说,编译器别无选择,只能将另一个TU中定义的函数视为具有所有可能的副作用。是的,最终会导致大量内存拷贝。然而,语言有一个解决方案,允许您就地构造对象(就在您需要构造对象的地方)。虽然这是一个很好的干净的解决方案,我想使用它,但它在我的上下文中不可用,因为我不能使用堆分配,因为我们的代码在嵌入式控制器上运行。lambda[&]{return internal{buffer,size(buffer)};
捕获当前上下文,根据编译器和上下文的大小,该上下文可能分配堆,也可能不分配堆。我们不将lambda与上下文一起使用,因为我们只想确保永远不使用堆。@XavierGroleau是的,std::函数使用堆(除非上下文足够小)Lambdas先生从来没有that@Frank谢谢!我改进了:)@Frank有另一种方法可以“修复”这个问题,但它会让MSVC不高兴…@XavierGroleau我在我的答案中添加了另一种解决方案(见底部的编辑)——你可能会发现它更适合你的情况。
#include <memory>
extern int foo(int);
extern void bar(int);
struct MyStruct {
int data;
MyStruct() {
auto val = std::make_unique<int>(12);
data = foo(*val);
}
};
int main(int argc, char** argv){
MyStruct x;
bar(x.data);
}
main:
sub rsp, 8
mov edi, 12
call foo(int)
mov edi, eax
call bar(int)
xor eax, eax
add rsp, 8
ret
#include <type_traits>
#include <array>
#include <cstdint>
#include <cstring>
using namespace std;
template<class F>
struct inplacer
{
F f_;
operator std::invoke_result_t<F&>() { return f_(); }
};
template<class F> inplacer(F) -> inplacer<F>;
struct Inner
{
Inner(uint8_t* data, size_t len)
: len_(len) // Let's assume arrayLength is always < 256
{
memcpy(payload_.data(), data, len*sizeof(*data));
}
std::array<uint8_t, 256> payload_;
size_t len_;
};
struct Outer
{
template<class T>
Outer(T&& inner): m_inner(std::forward<T>(inner)) {}
Inner m_inner;
};
struct OuterOuter
{
template<class T>
OuterOuter(T&& outer): m_outer(std::forward<T>(outer)) {}
Outer m_outer;
};
void addToThreadQueue(OuterOuter const&);
int main()
{
uint8_t buffer[4] = {1,2,3,4};
OuterOuter outerOuter{ inplacer{[&]{ return Inner{buffer, size(buffer)}; }} };
addToThreadQueue(outerOuter);
return 0;
}
main:
sub rsp, 280
mov rdi, rsp
mov DWORD PTR [rsp], 67305985
mov QWORD PTR [rsp+256], 4
call addToThreadQueue(OuterOuter const&)
xor eax, eax
add rsp, 280
ret