gnzlbg / static_vector

A dynamically-resizable vector with fixed capacity and embedded storage

Home Page:https://gnzlbg.github.io/static_vector

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

emplace back returns a reference in C++17

gnzlbg opened this issue · comments

Also, why are you doing a placement-new in emplace_back()? I would expect that inline_vector is a thin wrapper around a std::array + an end pointer, where you would just do

template<class... Args>
T& emplace_back(Args&&... args) 
{
    return *m_end++ = T(args...);
}

IIRC std::array always default constructs all elements, while vector does not (so you can use vector with non-default constructible elements), but I'll recheck that and come back to you.

Doing std::array<T, N> arr; leaves the values indeterminate. Doing std::array<T, N> arr{}; will value-initialize all elements to zero.
http://stackoverflow.com/questions/18295302/default-initialization-of-stdarray

Thanks for the link. I will check it out. The implementation isn't really up to date with the current proposal since I received a lot of feedback from Casey and the proposal changed in some aspects.

As soon as I fix the rest of the proposal issues I will update and clean the implementation so that we have something more concrete to discuss about.

PR are welcome though if you want to give it a try to reimplement it yourself. It is always nice to check that everything can be implemented from the proposal only, and I am kind of biased since I evolved both simultaneously.

OK, I'll wait for your code updates. BTW, the StackOverflow link I gave was answered by @CaseyCarter :)

In terms of implementation from spec: as a user I would like to know 100% that this container is nothing more than a push_back() interface on top of std::array so that one can do insertions through insert_iterators and use a few standard algorithms like copy and fill with it. Zero heap usage, no exceptions, no bounds checking. Just a prettified array.

Perhaps I should more carefully read the proposal. :) But the above would be my strong expectations.

BTW, in terms of a PR doing this in terms of array+pointer. I have some private code doing a bare-bones array with push_back and ranged-insert at the end. But I have no inclination at the moment to expand this to a full fledged vector-like entity (especially not with inserts in the middle and stuff).

My application is a simple game engine to store generated moves in it. E.g. in chess there are never more than 218 legal moves, so an inline_vector<Move, 256> (for alignment) is ideal for that. Only push_back() and op[] are needed for that, and push_back() should be nothing more than *m_end++ = value; (so no size checking!) in order to guarantee zero-overhead abstraction :)

If anything at all, I might propose a push_back extension to std::array (or propose this as a container adaptor).

I have another implementation of this (which is not available here yet), in which push_back does have a check, but it looks like this:

void push_back(T&& t) {
  #ifndef NDEBUG  // debug assertion: UB allows this, better QoI:
    assert(size() < capacity());  // assertion
  #else  // release build:
    __builtin_assume(size() < capacity()); // optimizer-hint
    // note: the boolean condition is never evaluated, if
    // it is not true it results in UB
  #endif
    construct(data(), std::forward<T>(t)); // basically: *data() = forward<T>(t);
    ++size_;
}

The only thing I wanted to point out is that some compilers like clang and MSVC have an assume intrinsic with zero run-time cost that one might use to hint optimizations on push-back methods (and others).

Whether an implementation uses internally 2 pointers, or 1 pointer and a size, or placement new, or a std::array, is kind of a QoI issue.

The proposal used to state more optimizations explicitly, but some of them had corner cases. So currently it only states that:

  • vector of zero capacity must be zero-sized (so that an object inheriting from it can benefit from EBO),
  • vector of trivial types must work in constexpr contexts
  • and then comments like, T does not need to be DefaultConstructible, which kind of hint at how an implementation should behave (whether an implementation uses a std::array, a C-array, or aligned_storage, is left open, but all of these different implementation strategy should not have a huge difference in their performance).

Whether it uses std::array internally or not, or maybe only in some cases, is really not specified. The problem of implementing it as you suggest using m_end pointer, is that you must correct the pointer on assignment/moves of the vector, while if instead of 1 array + pointer one uses 1 array + size, the size is always correct and can be just copied (and if T is trivial, the whole copy of the vector is just a memcpy).

I think it is more than art than science to specify the semantics of the class such that the most significant optimizations are a must, but at the same time to allow optimizations that we haven't thinked about to be implementable. I've tried to hit a sweet spot but I am open to be convinced otherwise about specific parts of the proposal (and have been often convinced to change my mind in the past, so if you believe something can be better, please argue with me here till I listen or convince you otherwise!).

I sincerely hope that as this gets advanced through the committee, some std libraries will implement it in std::experimental. This will result in very clever people carefully reading and implementing the proposal (which I think is fun to implement). I am pretty sure that we will learn new tricks and optimizations for them, that we might need to balance into the proposal.

Agreed on the efficiency under copy/move operations of array+size vs array+pointer. In that case I would like emplace_back to be as efficient as return m_arr[m_size++] = T(args...);.

You might want to cross-reference p0479r0 in regards to __builtin_assume (which may or maynot be standardized as a [[likely]] attribute.

Also, regarding zero-size objects: this probably rules out std::array<T, 0> as described in the Standard under [array.zero]. libstdc++ implements it as T arr[N ? N : 1];, so not an empty base candidate for N==1. Just an observation, I have no solution (maybe do a std::conditional on N) :)

I agree about not blocking future optimizations through the spec. OTOH, there is not much possible in terms of being more efficient than std::array. So I would start from the requirements on T imposed by std::array, and slowly add more requirements as you expand the interface (borrowing stuff from std::vector).

Eg the general container requirements state that Container u; has a single post-condition u.empty(). That would constrain the default constructor to set m_size = 0 and still avoid the initialization of all the elements, so no constraints on the default constructor of T.