23

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

Community
  • 1
  • 1
WhiZTiM
  • 21,207
  • 4
  • 43
  • 68
  • 1
    By ignoring the warning you are engaging in UB and what appears to work *now* with your *current* compiler (version) *may* break at random in the future with different compilers or different versions of your current one. Ignore UB at your peril. It *will* bite eventually. – Jesper Juhl Sep 13 '16 at 19:28
  • @JesperJuhl, warnings aren't always perfect, and the compiler often complains about issues that aren't relevant to a given codebase. I'm not a strict-aliasing expert, but it is possible that there is a perfectly standard-compliant solution to this that still generates these warnings – Aaron McDaid Sep 13 '16 at 19:34
  • Sure. That's possible. – Jesper Juhl Sep 13 '16 at 19:38
  • I think this solution is fine. The relevant memory is only read or written via pointers of type `base` or `derived`, so it's fine as they convert to each other nicely. If, however, you were to take a `const char *` and then try to `printf` it, then you might break the rules as you would have written via one type (`derived*` in the constructor) and then read via another type (`char *` inside `printf`). But even then, IIRC, there is special case for `char` in the strict-aliasing rules – Aaron McDaid Sep 13 '16 at 19:42
  • 1
    @AaronMcDaid, no, it's a violation, clear as day. – SergeyA Sep 13 '16 at 19:44
  • 4
    An educational explanation, perhaps as fully fledge answer, would be helpful – Aaron McDaid Sep 13 '16 at 19:45
  • As a side note, you probably want `derived>` for safety. – Barry Sep 13 '16 at 19:54
  • 2
    @AaronMcDaid, because you are only allowed to access an object through a a pointer of a different type in a limited set of circumstances. Neither of those apply here. – SergeyA Sep 13 '16 at 19:55
  • @SergeyA is there a question here on StackOverflow that details those limited circumstances? – Mark Ransom Sep 13 '16 at 20:49
  • @MarkRansom, they are detailed in 3.10/10. – SergeyA Sep 13 '16 at 20:54
  • @SergeyA I was looking for something a little more accessible. I saw your comment to an answer down below regarding 3.10/10 and it took me a while to find something. Here's a good candidate: https://stackoverflow.com/questions/14730896/where-does-the-c-standard-describe-the-casting-of-pointers-to-primitives – Mark Ransom Sep 13 '16 at 21:10
  • @MarkRansom, yes, this is the list. – SergeyA Sep 13 '16 at 21:10

3 Answers3

13

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.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • To save space, perhaps the `vtable` member of `Func` could be removed and `get_func_vtable` could be called explicitly each time. It will return the same address each time and hopefully this will be optimized away – Aaron McDaid Sep 13 '16 at 20:24
  • @AaronMcDaid No, because the type `T` is only available during construction, and `get_func_vtable` is a template class that creates a new function-level `static const vtable` and returns a pointer to it for each type `T` passed to it. This is called "type erasure": we are manually doing what the OP did with the `virtual` functions and destructor, thus avoiding UB. – Yakk - Adam Nevraumont Sep 13 '16 at 20:25
  • Interesting. Are you saying that `get_func_vtable` could return different addresses if called repeatedly with the same `T`? Or that multiple vtables would be created for the same `T`? That's hard to believe, so I suspect there is some other breakdown of communication involving me – Aaron McDaid Sep 13 '16 at 20:32
  • 1
    @AaronMcDaid I am saying that the `T` it is called with varies with each construction of `Func`. `T` is not a template parameter of `Func`, it is a template parameter of `Func::Func`. We only know it when we construct `Func`. The vtable maps invoke and destroy to "how to do it to a `T`". Later, `Func` does these operations *with no knowledge* (other than the vtable) of what it is performing the operations on. – Yakk - Adam Nevraumont Sep 13 '16 at 20:34
  • Sorry. I completely missed that. I was distracted by other issues and forgot that `T` was unknown at crucial points. In fact, that's the whole point of type erasure, and something I appreciated fully when I first read this question! I'm tempted to delete the two comments I've put on this answer. Any objection? – Aaron McDaid Sep 13 '16 at 20:46
  • @Yakk Can you expand on what you mean with that "pointer to B cannot be (safely) reinterpreted as pointer to A"? Do you mean `reinterpret_cast(b_ptr)` ? Because `A * a = b_ptr;` is perfectly valid AFAIK. – Daniel Jour Sep 14 '16 at 07:47
  • @dan Reinterpret cast does a different thing than implicit cast. Implicit can be safe while reinterpret is not. Especially `B*` to `void*` to `A*`. One can adjust the pointer value, the other does not, and the adjustment may be needed. – Yakk - Adam Nevraumont Sep 14 '16 at 10:54
  • @Yakk Ok, thank you :) then I understood you correctly (though I'm not sure everyone will correctly associate the "reinterpreted" in your sentence in the post with `reinterpret_cast` *and* differentiate it from implicit pointer conversion.) – Daniel Jour Sep 14 '16 at 11:00
  • Thanks @Yakk. Excellent as usual! I spent some time digging into [libstdc++'s](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/functional#L1755) and [libcxx's](https://github.com/llvm-mirror/libcxx/blob/master/include/functional#L1571) implementations, and they seem to use the *`base` -> ` – WhiZTiM Sep 14 '16 at 22:01
7

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.

Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • your reinterpret_casts are still undefined behavior. – SergeyA Sep 13 '16 at 19:40
  • 2
    @SergeyA nope; pre-P0137 we can rely on the "points to" rule ([basic.compound]/3), post-P0137 we can use `std::launder` per [intro.object]/3. – ecatmur Sep 13 '16 at 19:50
  • `reinterpret_cast(&c)->operator()();` - accessing an object through a pointer of a different type, which is not covered by well-known list of exceptions. – SergeyA Sep 13 '16 at 19:56
  • 3
    @SergeyA [intro.object]/3; [basic.compound]/4 doesn't hold because the type is not standard-layout, but it's still OK if the derived and base objects have the same address. – ecatmur Sep 13 '16 at 19:59
  • But address of aligned storage does not have the type of base! And &c returns address of aligned storage. – SergeyA Sep 13 '16 at 20:11
  • 2
    @ecatmur Without standard layout, how can the "same address" be expressed in standardese in a defined way? There is (at best) a derived at `&c` (ignoring aliasing): interpreting that as a `base` is seriously undefined, no? – Yakk - Adam Nevraumont Sep 13 '16 at 20:11
  • @Yakk, but why are you ignoring aliasing here? It is a problem right there. – SergeyA Sep 13 '16 at 20:13
  • 1
    @SergeyA Maybe; I'm stating it is undefined *even if we ignore aliasing*. You cannot take a pointer to a `class Base` that actually points to a `class Derived`, cast it to `void*`, then cast it to a `class Derived` unless you know the type is standard layout under the standard (and Base is the first parent of Derived), even without any aliasing argument. And in effect that is what the OP's and ecatmur's solution does. Aliasing may add another way the program is ill-formed on top of that. – Yakk - Adam Nevraumont Sep 13 '16 at 20:21
  • 1
    @Yakk, I see what you mean. Yes, of course you can't do this as well. – SergeyA Sep 13 '16 at 20:31
  • @Yakk "same address" is easy; cast to `void*` or `unsigned char*` and use `==`. As for pointer-interconvertibility: this is not required for using [ptr.launder]/1; we just need to ensure that the base subobject is *located at* the start of the storage, which is guaranteed by the ABI and can be checked at runtime. I agree that manual vtables are to be preferred. – ecatmur Sep 13 '16 at 20:34
  • 2
    @SergeyA aliasing occurs when you access memory using a type that is not the type of the object at that location. There is unambiguously an object of type `base` within the storage occupied by `c`, since a `new`-expression created such an object. The only issue here is whether there is a legal way to obtain a pointer to that object. – ecatmur Sep 13 '16 at 20:41
  • But address of `c` is a different type. This is unequivocally a violation if you just read standard literally. 3.10/10 lists all cases when you can access the object through a pointer to other type, none applies here. – SergeyA Sep 13 '16 at 20:53
  • 1
    @SergeyA we have cast the address of `c` to the base type of the complete object constructed within its storage, so the access is via a glvalue having: "— a type that is a (possibly cv-qualified) base class type of the dynamic type of the object". We are not trying to access the value of `c`. – ecatmur Sep 13 '16 at 20:58
  • @ecatmur, the type of glvalue of `&c` is a `pointer to c`. And this is what you are casting. – SergeyA Sep 13 '16 at 21:09
  • 1
    @SergeyA that's irrelevant, since we aren't accessing the object through that glvalue. The fact that we've used a pointer to that glvalue as an operand to a cast does *not* mean that we're accessing the object through that glvalue. – ecatmur Sep 13 '16 at 21:17
  • @ecatmur, yes, it does exactly that. Otherwise you can say that reintepret_cast never violates the rule, because any access through result of reinterpret_cast would be done through a different, type-correct glvalue. – SergeyA Sep 13 '16 at 21:23
  • 3
    @SergeyA no, because in `double d; int i = reinterpret_cast(d);` the object `d` of type `double` is being accessed via a glvalue of type `int`. In `aligned_union_t c; new (&c) int; int i = reinterpret_cast(c);` we are accessing an unnamed nested object of type `int` via a glvalue of type `int`, which is fine. – ecatmur Sep 13 '16 at 21:33
  • @ecatmur, Thanks alot! Totally forgot about `std::aligned_storage`. And I am just getting to know about `std::launder`. My understanding, `std::launder` basically instructs the compiler not to make assumptions about the value of the object in memory location being read. So, its usage is very suitable for objects stored in `std::aligned_storage`, Yes??. About the layout issues, [libstdc++](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/functional#L1755) and [libcxx](https://github.com/llvm-mirror/libcxx/blob/master/include/functional#L1571) not to have such fears... – WhiZTiM Sep 14 '16 at 22:37
  • I mean, for the layout issues: after spending some time trying to understand their implementation, their type erasure implementation is similar to my method. (no manual vtables). Though, I don't know if they use some compiler specific hooks to ensure type layouts for their libraries... – WhiZTiM Sep 14 '16 at 22:42
  • @ecatmur The "One danger not covered by the original question" only applies to the (reinterpret) cast, right? Doing `base * ptr = new (someAddress) derived{}` and only using ptr as in my answer is still safe, or? – Daniel Jour Sep 15 '16 at 05:42
  • 1
    @DanielJour yes, that part is fine. – ecatmur Sep 15 '16 at 08:51
  • @WhiZTiM yes, or libstdc++ and libcxx can just implicitly rely on compiler behavior, since they're written alongside the compilers. – ecatmur Sep 15 '16 at 08:51
2

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.

Daniel Jour
  • 15,896
  • 2
  • 36
  • 63
  • I did something like similar, though, not by storing the pointer in object, but temporarily `reinterpret_cast`ing it in the function scope the use it. However, from ecatmur's answer, I will additionally need to use [`std::launder`](http://en.cppreference.com/w/cpp/utility/launder) to prevent getting burned by the Compiler's optimizers. Thanks nonetheless. +1 – WhiZTiM Sep 14 '16 at 22:47