问题
I am writing a type-erased function wrapper similar to std::function. (Yes, I have seen similar implementations and even the p0288r0 proposal, but my use-case is quite narrow and somewhat specialized.). The heavily simplified code below illustrates my current implementation:
class Func{
alignas(sizeof(void*)) char c[64]; //align to word boundary
struct base{
virtual void operator()() = 0;
virtual ~base(){}
};
template<typename T> struct derived : public base{
derived(T&& t) : callable(std::move(t)) {}
void operator()() override{ callable(); }
T callable;
};
public:
Func() = delete;
Func(const Func&) = delete;
template<typename F> //SFINAE constraints skipped for brevity
Func(F&& f){
static_assert(sizeof(derived<F>) <= sizeof(c), "");
new(c) derived<F>(std::forward<F>(f));
}
void operator () (){
return reinterpret_cast<base*>(c)->operator()(); //Warning
}
~Func(){
reinterpret_cast<base*>(c)->~base(); //Warning
}
};
Compiled, GCC 6.1 warns about strict-aliasing :
warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
return reinterpret_cast<T*>(c)->operator()();
I also know about the strict-aliasing rule. On the other hand, I currently do not know of a better way to make use of small object stack optimization. Despite the warnings, all my tests passes on GCC and Clang, (and an extra level of indirection prevents GCC's warning). My questions are:
- Will I eventually get burned ignoring the warning for this case?
- Is there a better way for in-place object creation?
See full example: Live on Coliru
回答1:
First, use std::aligned_storage_t. That is what it is meant for.
Second, the exact size and layout of virtual types and their decendants is compiler-determined. Allocating a derived class in a block of memory then converting the address of that block to a base type may work, but there is no guarantee in the standard it will work.
In particular, if we have struct A {}; struct B:A{}; there is no guarantee unless you are standard layout that a pointer-to-B can be reintepreted as a pointer-to-A (especially throught a void*). And classes with virtuals in them are not standard layout.
So the reinterpretation is undefined behavior.
We can get around this.
struct func_vtable {
void(*invoke)(void*) = nullptr;
void(*destroy)(void*) = nullptr;
};
template<class T>
func_vtable make_func_vtable() {
return {
[](void* ptr){ (*static_cast<T*>(ptr))();}, // invoke
[](void* ptr){ static_cast<T*>(ptr)->~T();} // destroy
};
}
template<class T>
func_vtable const* get_func_vtable() {
static const auto vtable = make_func_vtable<T>();
return &vtable;
}
class Func{
func_vtable const* vtable = nullptr;
std::aligned_storage_t< 64 - sizeof(func_vtable const*), sizeof(void*) > data;
public:
Func() = delete;
Func(const Func&) = delete;
template<class F, class dF=std::decay_t<F>>
Func(F&& f){
static_assert(sizeof(dF) <= sizeof(data), "");
new(static_cast<void*>(&data)) dF(std::forward<F>(f));
vtable = get_func_vtable<dF>();
}
void operator () (){
return vtable->invoke(&data);
}
~Func(){
if(vtable) vtable->destroy(&data);
}
};
This no longer relies upon pointer conversion guarantees. It simply requires that void_ptr == new( void_ptr ) T(blah).
If you are really worried about strict aliasing, store the return value of the new expression as a void*, and pass that into invoke and destroy instead of &data. That is going to be beyond reproach: the pointer returned from new is the pointer to the newly constructed object. Access of the data whose lifetime has ended is probably invalid, but it was invalid before as well.
When objects begin to exist and when they end is relatively fuzzy in the standard. The latest attempt I have seen to solve this issue is P0137-R1, where it introduces T* std::launder(T*) to make the aliasing issues go away in an extremely clear manner.
The storage of the pointer returned by new is the only way I know of that clearly and unambiguously does not run into any object aliasing problems prior to P0137.
The standard did state:
If an object of type T is located at an address A, a pointer of type cv T* whose value is the address A is said to point to that object, regardless of how the value was obtained
the question is "does the new expression actually guarantee that the object is created at the location in question". I was unable to convince myself it states so unambiguously. However, in my own type erasure implementions, I do not store that pointer.
Practically, the above is going to do much the same as many C++ implementations do with virtual functions tables in simple cases like this, except there is no RTTI created.
回答2:
The better option is to use the Standard-provided facility for aligned storage for object creation, which is called aligned_storage:
std::aligned_storage_t<64, sizeof(void*)> c;
// ...
new(&c) F(std::forward<F>(f));
reinterpret_cast<T*>(&c)->operator()();
reinterpret_cast<T*>(&c)->~T();
Example.
If available, you should use std::launder to wrap your reinterpret_casts: What is the purpose of std::launder?; if std::launder is not available you can assume that your compiler is pre-P0137 and the reinterpret_casts are sufficient per the "points to" rule ([basic.compound]/3). You can test for std::launder using #ifdef __cpp_lib_launder; example.
Since this is a Standard facility, you are guaranteed that if you use it in accordance with the library description (i.e. as above) then there is no danger of getting burned.
As a bonus, this will also ensure that any compiler warnings are suppressed.
One danger not covered by the original question is that you're casting the storage address to a polymorphic base type of your derived type. This is only OK if you ensure that the polymorphic base has the same address ([ptr.launder]/1: "An object X that is within its lifetime [...] is located at the address A") as the complete object at construction time, as this is not guaranteed by the Standard (since a polymorphic type is not standard-layout). You can check this with an assert:
auto* p = new(&c) derived<F>(std::forward<F>(f));
assert(static_cast<base*>(p) == std::launder(reinterpret_cast<base*>(&c)));
It would be cleaner to use non-polymorphic inheritance with a manual vtable, as Yakk proposes, as then the inheritance will be standard-layout and the base class subobject is guaranteed to have the same address as the complete object.
If we look into the implementation of aligned_storage, it is equivalent to your alignas(sizeof(void*)) char c[64], just wrapped in a struct, and indeed gcc can be shut up by wrapping your char c[64] in a struct; although strictly speaking after P0137 you should use unsigned char rather than plain char. However, this is a rapidly evolving area of the Standard, and this could change in future. If you use the provided facility you have a better guarantee that it will continue to work.
回答3:
The other answer is basically rebuilding what most compilers do under the hood. When you store the pointer returned by the placement new, then there's no need to manually build vtables :
class Func{
struct base{
virtual void operator()() = 0;
virtual ~base(){}
};
template<typename T> struct derived : public base{
derived(T&& t) : callable(std::move(t)) {}
void operator()() override{ callable(); }
T callable;
};
std::aligned_storage_t<64 - sizeof(base *), sizeof(void *)> data;
base * ptr;
public:
Func() = delete;
Func(const Func&) = delete;
template<typename F> //SFINAE constraints skipped for brevity
Func(F&& f){
static_assert(sizeof(derived<F>) <= sizeof(data), "");
ptr = new(static_cast<void *>(&data)) derived<F>(std::forward<F>(f));
}
void operator () (){
return ptr->operator()();
}
~Func(){
ptr->~base();
}
};
Going from derived<T> * to base * is perfectly valid (N4431 §4.10/3):
A prvalue of type “pointer to cv D”, where D is a class type, can be converted to a prvalue of type “pointer to cv B”, where B is a base class (Clause 10) of D. [..]
And since the respective member functions are virtual, calling them through the base pointer actually calls the respective functions in the derived class.
来源:https://stackoverflow.com/questions/39477443/small-object-stack-storage-strict-aliasing-rule-and-undefined-behavior