0

I came across some weird compilation error related to std::ofstream. Let's say, I have one header file ofstream_test.hpp, and its corresponding source file ofstream_test.cpp.

ofstream_test.hpp:

#include <iostream>
#include <fstream>

class OfstreamTest {
public:
    OfstreamTest();

    std::ofstream m_strm_obj("output.txt");
};

ofstream_test.cpp:

#include "ofstream_test.hpp"

OfstreamTest::OfstreamTest() {
    std::cout << "ctor" << std::endl;
}

main.cpp:

#include "ofstream_test.hpp"

int main(int argc, char const *argv[]) {
    OfstreamTest obj;
    return 0;
}

The error:

$ g++ -std=c++17 main.cpp ofstream_test.cpp 
In file included from main.cpp:1:0:
ofstream_test.hpp:8:30: error: expected identifier before string constant
     std::ofstream m_strm_obj("output.txt");
                              ^~~~~~~~~~~~
ofstream_test.hpp:8:30: error: expected ‘,’ or ‘...’ before string constant
In file included from ofstream_test.cpp:1:0:
ofstream_test.hpp:8:30: error: expected identifier before string constant
     std::ofstream m_strm_obj("output.txt");
                              ^~~~~~~~~~~~
ofstream_test.hpp:8:30: error: expected ‘,’ or ‘...’ before string constant

If I try to use the open method, I also get the compilation error (a bit different though):

ofstream_test.hpp:

class OfstreamTest {
public:
    OfstreamTest();
    std::ofstream m_strm_obj;
    m_strm_obj.open("output.txt");
};

ofstream_test.cpp:

OfstreamTest::OfstreamTest() {
    std::cout << "ctor" << std::endl;
}

The error:

$ g++ -std=c++17 main.cpp ofstream_test.cpp 
In file included from main.cpp:1:0:
ofstream_test.hpp:9:5: error: ‘m_strm_obj’ does not name a type
     m_strm_obj.open("output.txt");
     ^~~~~~~~~~
In file included from ofstream_test.cpp:1:0:
ofstream_test.hpp:9:5: error: ‘m_strm_obj’ does not name a type
     m_strm_obj.open("output.txt");
     ^~~~~~~~~~

However, for the following cases, I'm not getting any compilation error:

  • Case 1:
class OfstreamTest {
public:
    OfstreamTest();
};
OfstreamTest::OfstreamTest() {
    std::cout << "ctor" << std::endl;
    std::ofstream m_strm_obj("output.txt");  // initialized the object in source file, instead of header file
}
  • Case 2:
class OfstreamTest {
public:
    OfstreamTest();
    std::ofstream m_strm_obj{"output.txt"};  // just changed () to {}
};
OfstreamTest::OfstreamTest() {
    std::cout << "ctor" << std::endl;
}
  • Case 3:
class OfstreamTest {
public:
    OfstreamTest();
    std::ofstream m_strm_obj;  // declared the object but didn't initialize in the header file
};
OfstreamTest::OfstreamTest() {
    std::cout << "ctor" << std::endl;
    m_strm_obj.open("output.txt");
}

I don't understand why?

Would greatly appreciate an in-depth answer (or at least an answer with some reference links)!

Milan
  • 1,743
  • 2
  • 13
  • 36
  • 1
    Member initializers is a late addition to C++. It was decided to avoid some old parsing problems by not allowing `()`, as it is way to easy to accidentally write `std::ofstream m_strm_obj();`, which is a function declaration. So you just have to use `{}` instead. – BoP Jul 15 '22 at 20:47
  • Thank you for your useful comment. What you said totally makes sense. However, I have a follow-up que: why does the compiler not get confused when I write `std::ofstream m_strm_obj("output.txt")`, somewhere in the source/implementation file? e.g. let's say in the constructor body. Why does the compiler not consider it as a function declaration? I tried and It doesn't give any compilation error; it works as well. I think I'm super-confused about this right now. Would appreciate some further inputs/clarifications :) – Milan Jul 15 '22 at 21:57
  • 1
    The compiler can see that `"output.txt"` cannot be part of a function declaration, so it must be part of a variable initialization. However, there are other examples of *extremely confusing* cases, known as [The Most Vexing Parse](https://stackoverflow.com/questions/7007817/a-confusing-detail-about-the-most-vexing-parse). This was well known when class member initialization was designed, so it was decided to allow `{}` but not `()`. In other places, initialization using `()` has been allowed since forever, and cannot easily be removed. – BoP Jul 16 '22 at 11:10

1 Answers1

2

std::ofstream m_strm_obj("output.txt"); is invalid syntax when declaring m_strm_obj as a class member (it is fine when declaring it as a local variable).

You simply can't initialize/construct a class data member (regardless of its type) with a value using parenthesis at the point where the member is being declared at class scope. The syntax rules of the C++ standard simply don't allow it, to avoid confusing the compiler with member function declarations.

But, since C++11 onward, you can initialize/construct a class data member at class scope using curly braces instead, eg:

class OfstreamTest {
public:
    OfstreamTest();

    std::ofstream m_strm_obj{"output.txt"};
};

Otherwise, you can instead use the member initializer list of the parent class's constructor, eg:

class OfstreamTest {
public:
    OfstreamTest();

    std::ofstream m_strm_obj;
};
OfstreamTest::OfstreamTest() : m_strm_obj("output.txt") {
    std::cout << "ctor" << std::endl;
}

Or, use open() in the constructor's body, eg:

class OfstreamTest {
public:
    OfstreamTest();

    std::ofstream m_strm_obj;
};
OfstreamTest::OfstreamTest() {
    std::cout << "ctor" << std::endl;
    m_strm_obj.open("output.txt");
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thank you Remy for the answer but what is so special about `std::ofstream`? If other `std` variable types can be initialized as a class member e.g. `std::string m_str = "milan"`, why not `std::ofstream`? – Milan Jul 15 '22 at 20:33
  • I just realized I can't initialize the `std::string` as a member variable like this `std::string m_str("milan")`. Getting the same compilation error. Don't get any error if I initialize it as a local variable or use `{}` instead of `()`. Why? or is this how C++ is supposed to work? – Milan Jul 15 '22 at 20:39
  • 2
    Yes, this is how C++ works. This has nothing to do with `ofstream` or `string` specifically. ALL types cannot be initialized inline with `()` inside a `class`/`struct` declaration (try with `int`, for example). The language syntax simply doesn't allow it. – Remy Lebeau Jul 15 '22 at 20:40
  • 1
    Apart from anything else, not a great place in the logic to be opening a file. Nor in the constructor neither. – Paul Sanders Jul 15 '22 at 20:42
  • 1
    @PaulSanders it might, it depends on the context it is being used for. For instance, if the class were being used as a logger, then opening the file when the logger is constructed might make perfect sense. – Remy Lebeau Jul 15 '22 at 20:45
  • 1
    @RemyLebeau Maybe. But what if it throws? – Paul Sanders Jul 15 '22 at 20:47
  • 1
    @PaulSanders So what if it does? Constructors are allowed to throw. If the `ofstream` throws in `OfstreamTest`'s constructor, the construction will be canceled, and the exception will propagate to the caller who is trying to construct `OfstreamTest`. They will know that something went wrong, and can choose whether to handle it or not. – Remy Lebeau Jul 15 '22 at 20:47
  • 2
    *Don't get any error if I initialize it as a local variable or use `{}` instead of `()`. Why?* In order to provide initialization for member variables, a new syntax had to be added to the language to support it, because otherwise the familiar syntax would not work (it'd syntactically collide). You may like this obligatory [The Nightmare of Initialization in C++](https://www.youtube.com/watch?v=7DTlWPgX6zs) by Nicolai Josuttis. An hour long presentation on the initialization syntax pain point in C++. – Eljay Jul 15 '22 at 21:05
  • 1
    Actually, I'm wrong. It doesn't throw if it can't open the file, it sets `failbit` on the stream. Personally, I think there's way too much happening in the constructor if it (tries to) open the file. Constructors should initialise the object and that's all. Failure conditions should be truly exceptional (invalid arguments, say). – Paul Sanders Jul 15 '22 at 21:11
  • 1
    @PaulSanders the `ofstream` constructor does not throw, no. But you can optionally use the `exceptions()` method to enable the `open()` method to throw on failure. But your point is taken. – Remy Lebeau Jul 15 '22 at 21:22
  • @RemyLebeau could you please throw some light on the 2nd compilation error I've mentioned in my question: using the `open` method in the header file? or that's also part of how C++ works? Thank you so much again! – Milan Jul 15 '22 at 21:41
  • @RemyLebeau one more thing, you said *"You simply can't initialize a class data member (regardless of its type) **inline** using parenthesis inside the scope of the class declaration."* -- what exactly does it mean when you say "initializing a class data member **inline**"? Do you mean `std::string m_str("milan")` is *inline*? What about `std::string m_str = "milan"`? Would greatly appreciate it if you can update your answer to cover this question as well as the one above. Thanks a ton in advance! – Milan Jul 15 '22 at 21:43
  • Thanks a lot, @RemyLebeau for all the replies. I would definitely take a look into the book recommendations. Hopefully, the last que (if it's too basic, please ignore): you also said *"The syntax rules of the C++ standard simply don't allow it, to avoid confusion with member function declarations."* -- I agree. However, why same is not true for local member initialization? I mean why does the compiler not get confused over there? e.g. In the source file, I can have a non-class function (AFAIK it's not that common but it's possible.) So, shouldn't the compiler give an error over there as well? – Milan Jul 15 '22 at 23:56
  • 1
    @Milan "*why same is not true for local member initialization? I mean why does the compiler not get confused over there?*" - there are cases where it *does* get confused. Read about the ["most vexing parse"](https://en.wikipedia.org/wiki/Most_vexing_parse), for instance. In general, if something *looks* like a function declaration, it is treated as a function declaration, even if you really intended it to be a variable declaration instead. – Remy Lebeau Jul 16 '22 at 00:06
  • 1
    @Milan Now, please, stop asking new questions in comments. We are getting off track of the original question, which has now been answered. Please ask any further questions as separate posts. – Remy Lebeau Jul 16 '22 at 00:07