I'm exploring move semantics in C 20 using "Beginning C 20 From Novice to Professional" by Horton and Van Weert. I'm using MS Visual Studio 2022 Version 17.2.5 as my IDE and I've tried a few different compiler optimization options under "C/C -> Optimization" and they don't seem to have any affect. The option currently selected is "Maximum Optimization (Favor Size) (/O1)" What is supposed to happen is that the number of times the 1000 element Array is moved, gets reduced from 20 to 10, as reflected in the output of the program:
The line "Array of 1000 elements moved" should only print 10 times if the move constructor is used by the compiler
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
Array of 1000 elements moved
The .cpp source and .ixx module files are listed below. Has anyone run into a similar situation and successfully tuned the compiler to avoid this issue?
Array.ixx
export module array;
import <stdexcept>;
import <string>;
import <utility>;
import <iostream>;
export template <typename T>
class Array
{
public:
explicit Array(size_t size); // Constructor
~Array(); // Destructor
Array(const Array& array); // Copy constructor
Array(Array&& array); // Move constructor
Array& operator=(const Array& rhs); // Copy assignment operator
void swap(Array& other) noexcept; // Swap member function
T& operator[](size_t index); // Subscript operator
const T& operator[](size_t index) const; // Subscript operator-const arrays
size_t getSize() const { return m_size; } // Accessor for m_size
private:
T* m_elements; // Array of type T
size_t m_size; // Number of array elements
};
// Constructor template
template <typename T>
Array<T>::Array(size_t size) : m_elements{ new T[size] {} }, m_size{ size }
{}
// Copy constructor template
template <typename T>
inline Array<T>::Array(const Array& array) : Array{ array.m_size }
{
std::cout << "Array of " << m_size << " elements copied" << std::endl;
for (size_t i{}; i < m_size; i)
m_elements[i] = array.m_elements[i];
}
// Move constructor template
template <typename T>
Array<T>::Array(Array&& moved)
: m_size{ moved.m_size }, m_elements{ moved.m_elements }
{
std::cout << "Array of " << m_size << " elements moved" << std::endl;
moved.m_elements = nullptr;
}
// Destructor template
template <typename T>
Array<T>::~Array() { delete[] m_elements; }
// const subscript operator template
template <typename T>
const T& Array<T>::operator[](size_t index) const
{
if (index >= m_size)
throw std::out_of_range{ "Index too large: " std::to_string(index) };
return m_elements[index];
}
// Non-const subscript operator template in terms of const one
// Uses the 'const-and-back-again' idiom
template <typename T>
T& Array<T>::operator[](size_t index)
{
return const_cast<T&>(std::as_const(*this)[index]);
}
// Template for exception-safe copy assignment operators
// (expressed in terms of copy constructor and swap member)
template <typename T>
inline Array<T>& Array<T>::operator=(const Array& rhs)
{
Array<T> copy{ rhs }; // Copy... (could go wrong and throw an exception)
swap(copy); // ... and swap! (noexcept)
return *this;
}
// Swap member function template
template <typename T>
void Array<T>::swap(Array& other) noexcept
{
std::swap(m_elements, other.m_elements); // Swap two pointers
std::swap(m_size, other.m_size); // Swap the sizes
}
// Swap non-member function template (optional)
export template <typename T>
void swap(Array<T>& one, Array<T>& other) noexcept
{
one.swap(other); // Forward to public member function
}
Ex18_01.cpp
import array;
import <string>;
import <vector>;
Array<std::string> buildStringArray(const size_t size)
{
Array<std::string> result{ size };
for (size_t i{}; i < size; i)
result[i] = "You should learn from you competitor, but never copy. Copy and you die.";
return result;
}
int main()
{
const size_t numArrays{ 10 };
const size_t numStringsPerArray{ 1000 };
std::vector<Array<std::string>> vectorOfArrays;
vectorOfArrays.reserve(numArrays);
for (size_t i{}; i < numArrays; i)
{
vectorOfArrays.push_back(buildStringArray(numStringsPerArray));
}
}
CodePudding user response:
The move constructor is being used, otherwise you would see messages saying "elements copied", not "elements moved". This is not an optimization. It is guaranteed by the language. The compiler options don't matter.
It is not guaranteed, no matter what optimization settings you use, that the line will be printed only 10 times. The compiler is allowed to perform NRVO (named return value optimization) in buildStringArray
, in which case there will be only 10 lines for the construction inside the std::vector
, but the compiler does not have to apply NRVO, nor can it be forced to do that. If it doesn't apply NRVO (for whatever reason), then the line may be printed up to 20 times, since each return result;
statement also causes a move construction.
If the book claims that it would be guaranteed that the line is printed only 10 times, then it is wrong. But according to OP's comment under this question it qualifies the statement with "normally", which I guess isn't incorrect. My tests on compiler explorer, putting everything in one translation unit, show that GCC, Clang and MSVC all apply NRVO, but I am not sure what the effect of separating into multiple translation units/modules will be.
Of course, if the compiler does not apply NRVO even with optimizations enabled, you may ask whether this is a missed optimization, but that is purely a quality-of-implementation issue, not a language issue.