Files, namespaces, and unique_ptr
Exceptions, STL, and ownership14 min readLesson 4 of 4
4.1 Reading and writing files
std::ofstream writes to a file, std::ifstream reads from one, and std::fstream does both. They come from <fstream> and behave like the console streams, so << and >> work the same way. A stream opens the file in its constructor and closes it in its destructor.
Opening can fail because of a missing directory, a permission problem, or a file that another program has locked. Check is_open() or test the stream's bool conversion after opening. A failed open is not an exception by default; the stream sets its failure state instead.
#include <fstream>
#include <iostream>
int main() {
std::ofstream out("scores.txt");
if (!out.is_open()) {
std::cout << "Could not open file\n";
return 1;
}
out << "Ada 90\n";
out << "Ravi 72\n";
out.close();
std::cout << "File written\n";
return 0;
}Output
File written
4.1.1 Reading line by line
std::getline(stream, line) reads up to the next newline, stores the text without the newline, and returns the stream. The return value converts to bool, so while (std::getline(in, line)) stops at end of file or on a read error.
Mixing >> and getline on the same stream often leaves a newline behind. >> skips leading whitespace and stops before the character after the number, which may be a newline. Call std::cin.ignore() before getline when that leftover blocks the read. The same rule applies when a program reads a file with a mix of formatted reads and line reads.
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ofstream out("notes.txt");
out << "first\nsecond\n";
out.close();
std::ifstream in("notes.txt");
std::string line;
int number = 1;
while (std::getline(in, line)) {
std::cout << number << ": " << line << "\n";
++number;
}
return 0;
}Output
1: first 2: second
4.1.2 Namespaces
A namespace groups names so that two libraries can define open without colliding. math::square and text::square are different functions. The :: operator selects a namespace, and names inside a namespace can refer to each other without the prefix.
using std::cout; brings one name into the current scope. using namespace std; brings every name, so keep it in small source files. A header that says using namespace forces that choice on every file that includes it.
#include <iostream>
#include <string>
namespace math {
int square(int value) { return value * value; }
}
namespace text {
std::string label() { return "square"; }
}
int main() {
std::cout << text::label() << ": " << math::square(6) << "\n";
return 0;
}Output
square: 36
4.1.3 Namespaces across files
Declarations in a header usually sit inside a namespace. The matching source file defines the same names inside the same namespace, and callers use the qualified name. This keeps the header's names out of the global namespace and lets several libraries coexist.
An unnamed namespace inside a source file gives its contents internal linkage, which is a modern replacement for static at file scope. It is useful for helper functions that should not be visible outside the translation unit.
4.1.4 One owner per object
Raw new returns a pointer that someone must delete. If the code returns early, throws, or forgets, the object leaks. Smart pointers wrap the pointer in a class whose destructor deletes the object, so the same RAII rule that closes files also frees memory.
std::unique_ptr<T> owns exactly one object and deletes it when the pointer goes out of scope. std::make_unique<T>(args...) creates the object and the pointer together, which avoids a separate new expression. The pointer cannot be copied, because two owners would both delete the same object.
#include <iostream>
#include <memory>
class Widget {
public:
explicit Widget(int id) : id_(id) { std::cout << "create " << id_ << "\n"; }
~Widget() { std::cout << "destroy " << id_ << "\n"; }
int id() const { return id_; }
private:
int id_;
};
int main() {
auto widget = std::make_unique<Widget>(7);
std::cout << "Widget " << widget->id() << "\n";
return 0;
}Output
create 7 Widget 7 destroy 7
4.1.5 Moving ownership
A unique_ptr can be moved. std::move(pointer) transfers ownership and leaves the source empty. A function that takes a unique_ptr by value takes ownership, and a function that takes a const reference or a raw pointer only borrows the object.
Containers of unique_ptr work because the elements can be moved. std::vector<std::unique_ptr<T>> stores each object separately and keeps the addresses stable when the vector grows. The container owns the pointers, and each pointer owns its object. Prefer unique_ptr over raw pointers for ownership, and reserve raw pointers for non-owning references.
#include <iostream>
#include <memory>
#include <vector>
int main() {
std::vector<std::unique_ptr<int>> values;
values.push_back(std::make_unique<int>(3));
values.push_back(std::make_unique<int>(8));
int total = 0;
for (const auto& value : values) {
total += *value;
}
std::cout << "Total: " << total << "\n";
return 0;
}Output
Total: 11
4.1.6 Related pointer types and mistakes
std::shared_ptr<T> allows several owners and frees the object when the last one goes away. It keeps a reference count, which costs time and memory. Use it only when ownership genuinely is shared. A std::weak_ptr<T> observes a shared object without keeping it alive.
Common mistakes include using a pointer after moving from it, creating two unique_ptrs from the same raw pointer, deleting a stack object, and forgetting #include <memory>. A file stream is also non-copyable; pass it by reference when a function needs to read or write.
4.1.7 Practice
- Write three names to a file with
ofstream, then read them back withgetline. - Check
is_open()on a path that does not exist and print a message. - Put two functions with the same name in separate namespaces and call both.
- Create a
unique_ptrto a class whose destructor prints, and watch the output at the end of the scope. - Move a
unique_ptrinto a vector and print the values through the pointers.
