Assignment Chef icon Assignment Chef
All English tutorials

Programming lesson

Building a File Utilities Namespace and File Class in C++: A Step-by-Step Guide

Learn how to implement a FileUtils namespace and a File class in C++ with practical examples. This tutorial covers string manipulation, constructors, getters, setters, and more.

C++ tutorial FileUtils namespace File class C++ findFileExtension hasWhitespaces C++ constructors getters setters C++ C++ OOP example C++ programming assignment help C++ file handling C++ namespace example C++ string functions C++ best practices C++ for beginners C++ project guide C++ coding tutorial

Introduction

In C++ programming, organizing code into namespaces and classes is essential for building maintainable applications. This tutorial walks you through creating a FileUtils namespace with utility functions and a File class that models file metadata. By the end, you'll have a solid understanding of how to handle filenames, extensions, and permissions in a structured way.

Think of it like organizing a gaming tournament: you need a set of rules (utility functions) to validate player entries, and a player profile (File class) to store each participant's info. Just as a tournament manager uses tools to check names and assign permissions, your program will use FileUtils to validate filenames and your File class to represent each file.

Task 1: Creating the FileUtils Namespace

A namespace is like a folder that groups related functions. Instead of scattering functions globally, we place them inside FileUtils to avoid naming conflicts and improve code organization.

Part A: Implementing findFileExtension

The findFileExtension function extracts the extension from a filename. The extension is defined as the last period and all characters after it. For example, "document.pdf" returns ".pdf". If there is no period or the filename is empty, it returns an empty string.

Header Declaration (FileUtils.hpp):

#ifndef FILEUTILS_HPP
#define FILEUTILS_HPP

#include <string>

namespace FileUtils {
    std::string findFileExtension(const std::string& filename);
    bool hasWhitespaces(const std::string& filename);
}

#endif

Implementation (FileUtils.cpp):

#include "FileUtils.hpp"
#include <cctype>

namespace FileUtils {
    std::string findFileExtension(const std::string& filename) {
        if (filename.empty()) return "";
        size_t dotPos = filename.rfind('.');
        if (dotPos == std::string::npos) return "";
        return filename.substr(dotPos);
    }
}

Notice the use of rfind to locate the last period. This ensures we capture the final extension even if the filename contains multiple dots (e.g., "archive.tar.gz" returns ".gz").

Testing: Write a small main program to verify:

#include <iostream>
#include "FileUtils.hpp"

int main() {
    std::cout << FileUtils::findFileExtension("report.docx") << std::endl;  // .docx
    std::cout << FileUtils::findFileExtension("notes") << std::endl;      // (empty)
    std::cout << FileUtils::findFileExtension("") << std::endl;           // (empty)
    return 0;
}

Part B: Implementing hasWhitespaces

This function checks if a filename contains any whitespace characters (spaces, tabs, newlines, etc.). It uses the std::isspace function from <cctype>.

Implementation (add to FileUtils.cpp):

bool hasWhitespaces(const std::string& filename) {
    for (char c : filename) {
        if (std::isspace(static_cast<unsigned char>(c))) return true;
    }
    return false;
}

This is crucial because filenames with spaces can cause issues in file systems and command-line operations. Just like in a video game where usernames with spaces are often disallowed to prevent parsing errors, your program will reject such filenames.

Task 2: Creating the File Class

Now we move from utility functions to object-oriented design. The File class encapsulates the attributes of a file: its name and read/write permissions.

Part A: Setting Up the Class

Basic Private Members

class File {
private:
    std::string filename_;
    bool readable_;
    bool writable_;
public:
    // constructors and methods
};

Constructors

Default Constructor: Initializes filename_ to "New_Text_Document.txt" and both permissions to true.

File() : filename_("New_Text_Document.txt"), readable_(true), writable_(true) {}

Parameterized Constructor: Accepts a filename and optional read/write booleans (default to true). It validates the filename using our utility functions:

  • If the filename is empty or contains whitespace, use the default filename.
  • If the filename has no extension (no period), append ".txt".
  • If the filename ends with a period (empty extension), append "txt".
  • Otherwise, use the filename as is.
  • Note: A filename like ".env" is allowed (it has an extension but no base name).
File(const std::string& filename, bool isReadable = true, bool isWritable = true)
    : readable_(isReadable), writable_(isWritable) {
    if (filename.empty() || FileUtils::hasWhitespaces(filename)) {
        filename_ = "New_Text_Document.txt";
        return;
    }
    std::string ext = FileUtils::findFileExtension(filename);
    if (ext.empty()) {
        filename_ = filename + ".txt";
    } else if (ext == ".") {
        filename_ = filename + "txt";
    } else {
        filename_ = filename;
    }
}

This constructor demonstrates how to reuse your utility functions to enforce business rules. It's like a game character creator that automatically corrects invalid names—if you try to name your hero with spaces, it falls back to a default name.

Part B: Getters and Setters

For readable_ and writable_

Getters are const to indicate they don't modify the object. Setters simply update the member.

bool isReadable() const { return readable_; }
void setReadable(bool new_permission) { readable_ = new_permission; }

bool isWritable() const { return writable_; }
void setWritable(bool new_permission) { writable_ = new_permission; }

For filename_

The getFilename getter returns the stored filename. The setFilename setter is more complex: it returns a boolean indicating success, and fails if the new filename is empty, contains spaces, or has a different extension than the current filename.

std::string getFilename() const { return filename_; }

bool setFilename(const std::string& newFilename) {
    if (newFilename.empty() || FileUtils::hasWhitespaces(newFilename)) return false;
    std::string oldExt = FileUtils::findFileExtension(filename_);
    std::string newExt = FileUtils::findFileExtension(newFilename);
    if (oldExt != newExt) return false;
    filename_ = newFilename;
    return true;
}

This ensures that renaming a file doesn't accidentally change its type. For example, you can't rename "data.csv" to "data.txt" because the extensions differ. This is similar to how a music streaming app might prevent changing a song's file format without re-encoding.

Putting It All Together

Here's a sample main function that demonstrates the full workflow:

#include <iostream>
#include "FileUtils.hpp"
#include "File.hpp"

int main() {
    // Test utilities
    std::cout << "Extension: " << FileUtils::findFileExtension("image.png") << std::endl;
    std::cout << "Has spaces: " << FileUtils::hasWhitespaces("my file.txt") << std::endl;

    // Test File class
    File defaultFile;
    std::cout << "Default filename: " << defaultFile.getFilename() << std::endl;

    File customFile("report", true, false);
    std::cout << "Custom filename: " << customFile.getFilename() << std::endl;  // report.txt

    // Try renaming
    if (customFile.setFilename("report_final")) {
        std::cout << "Rename succeeded: " << customFile.getFilename() << std::endl;
    } else {
        std::cout << "Rename failed" << std::endl;
    }

    // Try invalid rename (different extension)
    if (customFile.setFilename("data.csv")) {
        std::cout << "Rename succeeded" << std::endl;
    } else {
        std::cout << "Rename failed (extension mismatch)" << std::endl;
    }

    return 0;
}

Expected output:

Extension: .png
Has spaces: 1
Default filename: New_Text_Document.txt
Custom filename: report.txt
Rename succeeded: report_final
Rename failed (extension mismatch)

Conclusion

You've successfully built a small but robust file metadata system in C++. By separating utility functions into a namespace and encapsulating file properties in a class, you've created reusable, organized code. This pattern is common in larger projects—like how a cloud storage app might validate filenames before upload, or a game engine might manage asset files with permissions.

As a next step, consider extending the File class with additional attributes like file size or creation date, or add methods to simulate opening the file. The principles you've learned here will serve you well in more advanced C++ projects.