此代码仅用于说明问题。
#include <functional>
struct MyCallBack {
void Fire() {
}
};
int main()
{
MyCallBack cb;
std::function<void(void)> func = std::bind(&MyCallBack::Fire, &cb);
}
使用valgrind进行的实验表明,分配给func
的行在linux上使用gcc 7.1.1动态分配大约24个字节。
在实际代码中,我有一些不同的结构,所有结构都使用void(void)
成员函数,存储在~1000万std::function<void(void)>
中。
有什么办法可以避免在执行std::function<void(void)> func = std::bind(&MyCallBack::Fire, &cb);
时动态分配内存? (或以其他方式将这些成员函数分配给std::function
)
答案 0 :(得分:20)
不幸的是,std::function
的分配器已经在C ++ 17中删除了。
现在,在std::function
内避免动态分配的公认解决方案是使用lambdas而不是std::bind
。这确实有效,至少在GCC中 - 它有足够的静态空间来存储lambda,但没有足够的空间来存储binder对象。
std::function<void()> func = [&cb]{ cb.Fire(); };
// sizeof lambda is sizeof(MyCallBack*), which is small enough
作为一般规则,对于大多数实现,并且使用仅捕获单个指针(或引用)的lambda,您将使用此技术避免std::function
内的动态分配(通常也是更好的方法)其他答案表明。)
请记住,要实现这一目标,您需要保证此lambda将比std::function
更长久。显然,它并不总是可能的,有时你必须通过(大)副本捕获状态。如果发生这种情况,除了自己修改STL之外,目前无法消除函数中的动态分配(显然,在一般情况下不推荐,但可以在某些特定情况下完成)。
答案 1 :(得分:5)
作为已经存在且正确答案的补充,请考虑以下事项:
MyCallBack cb;
std::cerr << sizeof(std::bind(&MyCallBack::Fire, &cb)) << "\n";
auto a = [&] { cb.Fire(); };
std::cerr << sizeof(a);
这个程序为我打印24和8,同时包含gcc和clang。我不知道bind
在这里做了什么(我的理解是它是一个非常复杂的野兽),但正如你所看到的,与lambda相比,这里的效率几乎是荒谬的。
实际上,std::function
保证在从函数指针构造时不会分配,函数指针也是一个大小的单词。因此,从这种lambda中构造一个std::function
,只需要捕获指向一个对象的指针,也应该是一个单词,实际上应该永远不会分配。
答案 2 :(得分:1)
许多std :: function实现将避免分配并在函数类本身内部使用空间,而不是分配它所包含的回调是否足够小&#34;并且有琐碎的复制。但是,该标准并不要求这样,只建议它。
在g ++上,函数对象上的非平凡复制构造函数或超过16个字节的数据足以使其分配。但是如果你的函数对象没有数据并使用内置的复制构造函数,那么std :: function不会分配。 此外,如果您使用函数指针或成员函数指针,它将不会分配。
虽然不是您问题的直接部分,但它是您示例的一部分。 不要使用std :: bind。几乎在每种情况下,lambda都更好:更小,更好的内联,可以避免分配,更好的错误消息,更快的编译,列表继续。如果要避免分配,还必须避免绑定。
答案 3 :(得分:1)
我建议您根据具体用途使用自定义类。
虽然你不应该尝试重新实现现有的库功能,因为库的功能会更加经过测试和优化,但它也适用于< em>一般情况。如果您的示例中存在特定情况,并且标准实现不能满足您的需求,您可以探索实施针对您的特定用例量身定制的版本,您可以根据需要进行测量和调整。
所以我创建了一个类似于std::function<void (void)>
的类,它只适用于方法并且具有所有存储空间(没有动态分配)。
我亲切地称它Trigger
(受你的Fire
方法名称启发)。如果你愿意,请给它一个更合适的名字。
// helper alias for method
// can be used in user code
template <class T>
using Trigger_method = auto (T::*)() -> void;
namespace detail
{
// Polymorphic classes needed for type erasure
struct Trigger_base
{
virtual ~Trigger_base() noexcept = default;
virtual auto placement_clone(void* buffer) const noexcept -> Trigger_base* = 0;
virtual auto call() -> void = 0;
};
template <class T>
struct Trigger_actual : Trigger_base
{
T& obj;
Trigger_method<T> method;
Trigger_actual(T& obj, Trigger_method<T> method) noexcept : obj{obj}, method{method}
{
}
auto placement_clone(void* buffer) const noexcept -> Trigger_base* override
{
return new (buffer) Trigger_actual{obj, method};
}
auto call() -> void override
{
return (obj.*method)();
}
};
// in Trigger (bellow) we need to allocate enough storage
// for any Trigger_actual template instantiation
// since all templates basically contain 2 pointers
// we assume (and test it with static_asserts)
// that all will have the same size
// we will use Trigger_actual<Trigger_test_size>
// to determine the size of all Trigger_actual templates
struct Trigger_test_size {};
}
struct Trigger
{
std::aligned_storage_t<sizeof(detail::Trigger_actual<detail::Trigger_test_size>)>
trigger_actual_storage_;
// vital. We cannot just cast `&trigger_actual_storage_` to `Trigger_base*`
// because there is no guarantee by the standard that
// the base pointer will point to the start of the derived object
// so we need to store separately the base pointer
detail::Trigger_base* base_ptr = nullptr;
template <class X>
Trigger(X& x, Trigger_method<X> method) noexcept
{
static_assert(sizeof(trigger_actual_storage_) >=
sizeof(detail::Trigger_actual<X>));
static_assert(alignof(decltype(trigger_actual_storage_)) %
alignof(detail::Trigger_actual<X>) == 0);
base_ptr = new (&trigger_actual_storage_) detail::Trigger_actual<X>{x, method};
}
Trigger(const Trigger& other) noexcept
{
if (other.base_ptr)
{
base_ptr = other.base_ptr->placement_clone(&trigger_actual_storage_);
}
}
auto operator=(const Trigger& other) noexcept -> Trigger&
{
destroy_actual();
if (other.base_ptr)
{
base_ptr = other.base_ptr->placement_clone(&trigger_actual_storage_);
}
return *this;
}
~Trigger() noexcept
{
destroy_actual();
}
auto destroy_actual() noexcept -> void
{
if (base_ptr)
{
base_ptr->~Trigger_base();
base_ptr = nullptr;
}
}
auto operator()() const
{
if (!base_ptr)
{
// deal with this situation (error or just ignore and return)
}
base_ptr->call();
}
};
用法:
struct X
{
auto foo() -> void;
};
auto test()
{
X x;
Trigger f{x, &X::foo};
f();
}
警告:仅测试编译错误。
您需要彻底测试它的正确性。
您需要对其进行分析,看看它是否具有比其他解决方案更好的性能。这样做的好处是因为它可以在实施中进行调整,以提高特定场景的性能。
答案 4 :(得分:0)
运行此小技巧,它可能会打印出无需分配内存即可捕获的字节数:
#include <iostream>
#include <functional>
#include <cstring>
void h(std::function<void(void*)>&& f, void* g)
{
f(g);
}
template<size_t number_of_size_t>
void do_test()
{
size_t a[number_of_size_t];
std::memset(a, 0, sizeof(a));
a[0] = sizeof(a);
std::function<void(void*)> g = [a](void* ptr) {
if (&a != ptr)
std::cout << "malloc was called when capturing " << a[0] << " bytes." << std::endl;
else
std::cout << "No allocation took place when capturing " << a[0] << " bytes." << std::endl;
};
h(std::move(g), &g);
}
int main()
{
do_test<1>();
do_test<2>();
do_test<3>();
do_test<4>();
}
使用gcc version 8.3.0
可打印
捕获8个字节时没有分配。
捕获16个字节时没有分配。
捕获24个字节时调用了malloc。
捕获32个字节时调用了malloc。