define cindy const define cindy const - 1 month ago 12
C++ Question

Using static class variables without allocationg them

Background



So earlier today I was implementing a thin wrapper to
std::ofstream
that allowed me to write to .csv files easily. I wanted to override the
<<
operator to write value followed by a comma, and then when the time came for a new line, I would print a backspace character and then a new line. I decided to implement the new line behaviour as a template specialization as follows:

// *this << value; => prints the value to the csv file
// *this << CsvWriter::BREAK; => goes to the next line of the csv file
// Example:
// CsvWriter csv("test.csv", {"Col 1", "Col 2", "Col 3"});
// csv << "Value 1" << 2 << 3.25 << CsvWriter::BREAK;
class CsvWriter {
public:
CsvWriter(const char *fname, std::vector<const char *> headers);
CsvWriter() = delete;
CsvWriter(const CsvWriter &) = delete;
CsvWriter &operator=(const CsvWriter &) = delete;
~CsvWriter() = default;

// Used for the template specialization below
static struct LineBreak {} BREAK;

template <typename T>
CsvWriter &operator<<(T t);

private:
std::ofstream out_;
bool first_ = true; // true when printing first element of line
};

template <typename T>
CsvWriter &CsvWriter::operator<<(T t) {
if (!first_) {
out_ << ',';
} else {
first_ = false;
}
out_ << t;
return *this;
}

// This is the specialization for newlines.
// If anything of type LineBreak is passed to this function, this executes.
template <>
CsvWriter &CsvWriter::operator<<(LineBreak) {
out_ << std::endl;
first_ = true; // Reset first for next line
return *this;
}

// The constructor gives an example use of the template specialization
CsvWriter::CsvWriter(const char *fname, std::vector<const char *> headers)
: out_(fname) {
for (const char *header : headers) {
*this << header; // Prints each string in header
}
*this << BREAK; // Goes to the next line of the csv
}


Brief Explanation



This code works perfectly as is, and compiles with no complaints in gcc. However, I noticed that I was technically not allocating memory to the value
BREAK
. So to check it out, I tried printing the value of
&CsvWriter::BREAK
and ran into a linking error (which makes sense, I'm asking for the address of something that's not in memory). And furthermore, if I add the line
CsvWriter::LineBreak CsvWriter::BREAK;
after the class definition, then I can print the value of
&CsvWriter::BREAK
no problem (which also makes sense because now I've given it memory.

From this, I can puzzle together that if the value is never used in any compilation unit, the linker will never look for the value and never complain about it. But if I use it (such as grabbing the address), the linker will try and link the name, and not find anything.

Question



While I find this result very useful for what I'm trying to do, I'm curious, is this technically against the C++11 standard? Or is this perfectly valid code? If not, is there a better way to do this with a similarly clear and simple interface?

Answer

This is technically an ill-formed program.

What is likely happening is that the compiler is smart enough to see that the value of the parameter to the overloaded function is never used. Consequently, the compiler does not emit the code to pass the unused BREAK value to the overloaded << operator. Since no reference to the symbol gets generated, the linker does not complain.

But if you explicitly generate a reference to the object, the link will fail, of course.

The C++ standard does not require the compiler to perform this optimization, as such this is ill-formed. So, technically this is, indeed, not allowed by the C++ standard.

Comments