Copy constructor for custom array class being invoked when copy elision was expected

124 views Asked by At

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.

2

There are 2 answers

0
HolyBlackCat On

std::array doesn't have any custom constructors. It only has a public array member, making it an aggregate:

template <typename ValueType, std::size_t length>
class StaticArray
{
  public:
    ValueType elems[length];
};

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, and std::array can.

0
Mike Tyukanov On

Copy/move elision is the absence of temporary materialization:

Another way to describe C++17 mechanics is "unmaterialized value passing" or "deferred temporary materialization": prvalues are returned and used without ever materializing a temporary.

So to understand in which cases copy elision doesn't happen, we have to see when temporary materialization happens, and that would be, obviously, the section Temporary materialization:

Temporary materialization occurs in the following situations:
...
when initializing an object of type std::initializer_list from a braced-init-list;

Looks like that's your case, with your constructor taking std::initializer_list.