As stated in the previous post, i'm looking for feedback on the updated version of the code posted.
A few things specifically I'm looking for feedback on, are:
I switched to using the
std::aligned_storage
as suggested in the previous post, but on the cppreference.com - std::aligned_storage page, in the examples it says// note: needs std::launder as of C++17
above thereinterpret_cast
's. Would that mean, that the launder function should just wrap the cast, and if so, it states that its needed for c++17, which is what I'm using, but I'd also like the code to be compatible back to c++11. What would be the recommended approach in this situation?I was considering making a private function, something like:
#include <new> // ... [[nodiscard]] const T* ptr_to(std::size_t pos) const { return std::launder(reinterpret_cast<const T*>(&data[pos])); }
to remove some of the code duplication, I'm still trying to gain a better understanding of
const
correctness, would I need a const version and a non-const version of this function, similar to thepeek
function?Like I mentioned in a previous point, I'm wanting to target c++17 primarily, but also be backward compatible down to c++11. Language features such as
[[nodiscard]]
andstd::launder
are c++17 only, is there a standard way(using macros or otherwise) to accommodate for this?The function
void push(const T& item) noexcept
takes a const ref and usesstd::move
internally on it, is that reasonable? or should that only be done to params such asT&& item
? Should I just delete thestd::move
in that function?Any suggestions for cutting down on code duplication are welcome.
CircularBuffer.h
#pragma once
#include <cassert>
#include <type_traits>
namespace datastructures {
template<class T, std::size_t N>
class CircularBuffer {
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t head = 0;
std::size_t tail = 0;
bool isFull = false;
public:
template<typename ...Args>
void emplace_push(Args&&... args) noexcept {
assert(!isFull && "Attempting to push item into full buffer!");
new (&data[head]) T(std::forward<Args>(args)...);
head = ++head % N;
isFull = head == tail;
}
void push(const T& item) noexcept {
assert(!isFull && "Attempting to push item into full buffer!");
new (&data[head]) T(std::move(item));
head = ++head % N;
isFull = head == tail;
}
T pop() noexcept {
assert(!is_empty() && "Attempting to pop item from empty buffer!");
auto ptr = reinterpret_cast<T*>(&data[tail]);
auto result = std::move(*ptr);
ptr->~T();
tail = ++tail % N;
isFull = false;
return result;
}
[[nodiscard]] constexpr T& peek() noexcept {
assert(!is_empty() && "Attempting to peek in empty buffer!");
return *reinterpret_cast<T*>(&data[tail]);
}
[[nodiscard]] constexpr const T& peek() const noexcept {
assert(!is_empty() && "Attempting to peek in empty buffer!");
return *reinterpret_cast<const T*>(&data[tail]);
}
[[nodiscard]] constexpr bool is_empty() const noexcept {
return !isFull && tail == head;
}
[[nodiscard]] constexpr std::size_t get_capacity() const noexcept {
return N;
}
[[nodiscard]] constexpr std::size_t get_size() const noexcept {
if (isFull)
return N;
if (head >= tail)
return head - tail;
return N + head - tail;
}
};
}
Edit: I started using the macro:
#ifdef __has_cpp_attribute
# if __has_cpp_attribute(nodiscard)
# define NO_DISCARD [[nodiscard]]
# else
# define NO_DISCARD
# endif
#else
# define NO_DISCARD
#endif
for the [[nodiscard]]
on the functions. Is this reasonably portable / acceptable or is there anything better, also still looking for a good way to conditionally add the std::launder
part if available in the language.
1 Answer 1
You say you want to
target c++17 primarily, but also be backward compatible down to c++11.
Consider changing this to
Because you can't really use most of the c++17 stuff here. You have to write code in c++11, but also take care of c++17 compatibility. To be honest, I don't think this is a good idea, but I will respect your opinion anyway.
I don't know much about std::launder
,
but I remember someone saying that the code with theoretical undefined behavior
will do the right job on any (sane) implementation.
I'm not gonna care much about it here.
Bugs
Your code does not implement RAII and is not exception-safe. You should add a destructor to clean up the contents. You should also implement (copy|move) (constructors|assignments). The copy-and-swap idiom can help you.
Note that the automatically generated destructor does not call the destructor of
T
, and the automatically generated copy and move stuff do not call the copy and move stuff ofT
. They just copy the rawchar
s, which is clearly not intended.Here's an example of what a destructor might look like:
~CircularBuffer() { if (is_empty()) return; std::size_t i = tail; do { reinterpret_cast<T*>(&data[i])->~T(); i = (i + 1) % N; } while (i != head); }
And the copy constructor and move constructor:
CircularBuffer(const CircularBuffer& other) :head{other.head}, tail{other.tail}, isFull{other.isFull} { // note: not exception safe if (!is_empty()) { for (; i != head; i = (i + 1) % N) new (&data[i]) T(other.data[i]); } } CircularBuffer(CircularBuffer&& other) :head{other.head}, tail{other.tail}, isFull{other.isFull} { // note: not exception safe if (!is_empty()) for (std::size_t i = tail; i < head; i = (i + 1) % N) new (&data[i]) T(std::move(other.data[i])); }
Suggestions
The following code is confusing:
head = ++head % N;
Granted, it works as intended since c++11, but still, consider changing to the more readable
head = (head + 1) % N;
This will prevent any confusion.
push
accepts aconst T&
andstd::move
s from it. This makes no sense.std::move(item)
will return aconst T&&
and usually binds toconst T&
copy constructors. Pass-by-value instead:void push(T item) noexcept { /* ... */ }
This will handle both copying and moving correctly.
Moreover,
push
should probably delegate toemplace_push
to reduce code duplication.void push(T item) noexcept { emplace_push(std::move(item)); }
pop
should not return the popped value because it cannot do this in a safe way.1 Instead, letpop
returnvoid
and letpeek
do its job.
By the way, there is much debate on #pragma once
(see, for example, #pragma once
vs include guards?). I think it is OK here, but others may disagree.
1 The move constructor of T
may throw an exception,
in which case the popped object is leaked and cannot be restored.
-
\$\begingroup\$ So I've made almost all the changes mentioned aside from the constructor related ones, firstly, are copy/move/etc constructors not automatically generated if no constructor is defined? I thought they were? Aside from that, how are template parameters handled when dealing with copy constructors etc, would you be able to give a very basic example with at least the aligned storage member variable? Thanks. \$\endgroup\$Hex– Hex2019年05月25日 05:42:17 +00:00Commented May 25, 2019 at 5:42
-
\$\begingroup\$ @Hex I have updated my answer to answer your first question. For the example, wait a moment \$\endgroup\$L. F.– L. F.2019年05月25日 05:46:00 +00:00Commented May 25, 2019 at 5:46
-
\$\begingroup\$ I also added a "move push" ` void push(T&& item) noexcept { assert(!isFull && "error: buffer is full!"); new (&data[head]) T(std::move(item)); increment_head(); }` is it proper to use the std::move here or is that redundant as the
item
is already typeT&&
? \$\endgroup\$Hex– Hex2019年05月25日 05:49:02 +00:00Commented May 25, 2019 at 5:49 -
1\$\begingroup\$ @Hex You have to still use
move
becauseitem
is an lvalue although it is of typeT&&
. That said, you can unify the copy push and move push a pass-by-value push (see point 2 and 3). \$\endgroup\$L. F.– L. F.2019年05月25日 05:50:10 +00:00Commented May 25, 2019 at 5:50 -
1\$\begingroup\$ @Hex Yes, exactly. The pass-by-value
push
shown in point 2 and 3 handles both copy and move properly. \$\endgroup\$L. F.– L. F.2019年05月25日 05:54:15 +00:00Commented May 25, 2019 at 5:54