I started writing up my own versions of C++ STL classes for practice and stumbled across a weird issue with std::array (stl::StaticArray is what I called mine). When constructing a 2D stl::StaticArray like so: stl::StaticArray<stl::StaticArray<int, 5>, 5> data{ { 0, 1, 2, 3, 4 }, { 5, 6, 7, 8, 9 }, { 10, 11, 12, 13, 14 }, { 15, 16, 17, 18, 19 }, { 20, 21, 22, 23, 24 } }, the copy constructor is being called, clearly indicating that copy elision is not being performed on the 5 temporary sub-arrays being constructed.
The source for StaticArray.hpp:
#pragma once
#include "Common/types.hpp"
#ifdef STL_COPY_WARNINGS
#include <iostream>
#endif
#ifdef STL_MOVE_WARNINGS
#include <iostream>
#endif
#include <initializer_list>
namespace stl {
template <typename ValueType, u64 length> class StaticArray {
public:
StaticArray() = default;
StaticArray(ValueType const& value) noexcept {
for (u64 i = 0; i < length; ++i) { m_Data[i] = value; }
}
StaticArray(StaticArray const& data) noexcept {
#ifdef STL_COPY_WARNINGS
std::cout << "StaticArray Copied: " << std::hex << reinterpret_cast<u64>(&data) << " -> " << std::hex << reinterpret_cast<u64>(this) << '\n';
#endif
for (u64 i = 0; i < length; ++i) { m_Data[i] = data.m_Data[i]; }
}
StaticArray(StaticArray&& data) noexcept {
#ifdef STL_MOVE_WARNINGS
std::cout << "StaticArray Moved: " << std::hex << reinterpret_cast<u64>(&data) << " -> " << std::hex << reinterpret_cast<u64>(this) << '\n';
#endif
for (u64 i = 0; i < length; ++i) { m_Data[i] = data.m_Data[i]; }
}
StaticArray(std::initializer_list<ValueType> data) noexcept {
for (u64 i = 0; auto const& entry : data) {
m_Data[i++] = entry;
}
}
~StaticArray() noexcept = default;
StaticArray& operator= (StaticArray const& data) noexcept {
#ifdef STL_COPY_WARNINGS
std::cout << "StaticArray Copied: " << std::hex << reinterpret_cast<u64>(&data) << " -> " << std::hex << reinterpret_cast<u64>(this) << '\n';
#endif
for (u64 i = 0; i < length; ++i) { m_Data[i] = data.m_Data[i]; }
return *this;
}
StaticArray& operator= (StaticArray&& data) noexcept {
#ifdef STL_MOVE_WARNINGS
std::cout << "StaticArray Moved: " << std::hex << reinterpret_cast<u64>(&data) << " -> " << std::hex << reinterpret_cast<u64>(this) << '\n';
#endif
for (u64 i = 0; i < length; ++i) { m_Data[i] = data.m_Data[i]; }
return *this;
}
[[nodiscard]] ValueType& operator[] (u64 const index) noexcept { return m_Data[index]; }
[[nodiscard]] ValueType const& operator[] (u64 const index) const noexcept { return m_Data[index]; }
[[nodiscard]] ValueType* begin() noexcept { return m_Data; }
[[nodiscard]] ValueType* end() noexcept { return m_Data + length; }
[[nodiscard]] ValueType const* const begin() const noexcept { return m_Data; }
[[nodiscard]] ValueType const* const end() const noexcept { return m_Data + length; }
[[nodiscard]] ValueType const* const cbegin() const noexcept { return m_Data; }
[[nodiscard]] ValueType const* const cend() const noexcept { return m_Data + length; }
[[nodiscard]] inline constexpr u64 size() const noexcept { return length; }
[[nodiscard]] ValueType* data() noexcept { return m_Data; }
[[nodiscard]] ValueType const* const data() const noexcept { return m_Data; }
private:
ValueType m_Data[length];
};
}
It has always been my understanding that C++ elides copies on non-reference-bound temporaries and I only caught this because I decided to test it on a whim. Would love some feedback / ideas on why you believe my program may be behaving this way and I welcome suggestions on what I could do to resolve the issue.
std::arraydoesn't have any custom constructors. It only has a public array member, making it an aggregate:This is all you need for the brace initialization to work. This also gives you copy elision, so initialization can work without any copies or moves.
You can also remove all
operator=s, the compiler will generate them for you.You need a specialization for
length == 0, because plain arrays can't have zero size, andstd::arraycan.