C++ / Programming

C++ copy constructor and elision

Lets say we have a file class object with a file buffer, like so:

#include <unistd.h> #include <stdio.h> #include <string.h> #include <iostream> using std::cout; using std::endl; namespace talregcom { class BufferFile { private: int fileDescriptor; char *fileData; public: BufferFile(); ~BufferFile(); public: int open(const char *fileName); int readData(); }; BufferFile::BufferFile() { cout << "creating BufferFile" << endl; fileDescriptor = 0; fileData = new char[1024]; } int BufferFile::open(const char *fileName) { // open logic return 0; } int BufferFile::readData() { // read some data return 0; } BufferFile::~BufferFile() { cout << "terminating BufferFile" << endl; if (fileDescriptor != 0) { ::close(fileDescriptor); fileDescriptor = 0; } if (fileData != nullptr) { cout<<"filedata="<<static_cast<void*>(fileData)<<endl; delete[] fileData; fileData = nullptr; } } }
Code language: C++ (cpp)

This is not an uncommon pattern. Now, what do we need to do, if somewhere we’ve created a function like this?

BufferFile OpenLogFile() { BufferFile file; file.open("my_log_file.txt"); return file; } int main() { BufferFile logfile=OpenLogFile(); // do things with logfile }
Code language: C++ (cpp)

Can you tell what the output will be? Well, you can guess, but it actually depends on the compiler. Old C++ programmers will never pass a code like this; back in the old days, the code will fail, 100% of the time. The reason? The data pointer is allocated once, but freed twice. How is that? The default copy constructor. However, these days, compilers are smart; a lack of a copy constructor, or it’s presence, wouldn’t change much here; the compiler just won’t call them, and will not create a temporary object at all. If we want to check the lack of copy constructor (thus having our program crash) we can compile like this:

g++ --std=c++17 -fno-elide-constructors main2.cpp -o bin/m2
Code language: Bash (bash)

Here we are basically telling the compiler that we don’t want any help in optimization, we got it. Now, a copy will be created, and since we didn’t provide one, the default shallow copy will be applied. Result? seg-fault. Now, the compiler is doing a great work! So, if we are not planning on using this switch, should we even include a copy constructor? The answer is yes. If we have this code, the elision will not be used:

#include "buffer_file.hpp" using talregcom::BufferFile; BufferFile OpenLogFile() { BufferFile file; file.open("my_log_file.txt"); return file; } int main() { BufferFile logfile=OpenLogFile(); BufferFile logfile2=logfile; return 0; }
Code language: C++ (cpp)

If you run this code, in any compiler, this will result in seg fault. Here the compiler can’t really tell what do we want to do with logfile2, and hence, will have to create a copy. Here is out copy constructor:

BufferFile::BufferFile(const BufferFile &rhs) { cout << "copying BufferFile" << endl; fileDescriptor = rhs.fileDescriptor; fileData = new char[1024]; if (rhs.fileData != nullptr) ::memcpy(fileData, rhs.fileData, 1024); }
Code language: C++ (cpp)

This bring another interesting question: with elision in place, should we even bother with a move constructor? Something to think about.

Leave a Reply

Your email address will not be published. Required fields are marked *