Applying the Google C++ Style Guide in Modern C++ (C++23)

The Google C++ Style Guide is a widely respected set of best practices for writing C++ code. Combined with the Tips of the Week (ToTW) from Abseil, it provides a robust framework for developing maintainable and efficient C++ applications. A key tenet of this style guide is avoiding exceptions for error handling, favoring alternative approaches like status codes. Resource Acquisition Is Initialization (RAII) is another crucial technique emphasized for robust resource management.

This article explores practical implementations of these principles in modern C++ (specifically C++23, using GCC 12.2.0 with -std=c++2b). We’ll delve into solutions for common challenges, such as error handling without exceptions using std::expected, managing complex object initialization, and dealing with polymorphism in RAII contexts, all while adhering to the spirit of the C++ Google Style Guide. While libraries like Abseil offer excellent tools such as absl::Status and absl::StatusOr, we will explore standard library alternatives and lightweight implementations suitable for projects where external dependencies are minimized.

Problem #1: Effective Error Returns in C++

In line with the c++ google style guide‘s preference for non-exception-based error handling, C++23’s std::expected provides a powerful and type-safe way to represent functions that can either return a value or an error. Let’s consider a scenario where a function needs to return an integer, but might fail under certain conditions. Instead of throwing an exception, we can use std::expected<int, Error> to explicitly signal potential failure.

// Compile with g++ -std=c++2b example_1.cc -o example_1
// Run with ./example_1
#include <iostream>
#include <expected>

enum class Error { InvalidArgument, Unavailable };

std::string ToString(const Error& e) {
  if (e == Error::InvalidArgument) {
    return "InvalidArgument";
  }
  if (e == Error::Unavailable) {
    return "Unavailable";
  }
  return "Unknown";
}

std::expected<int, Error> GetInt(bool want_success) {
  if (want_success) {
    return 42;
  }
  return std::unexpected(Error::Unavailable);
}

void Example(bool want_success) {
  auto result = GetInt(want_success);
  std::cout << "Got ";
  if (result.has_value()) {
    std::cout << result.value() << std::endl;
  } else {
    std::cout << ToString(result.error()) << std::endl;
  }
}

int main(int argc, char* argv[]) {
  Example(true);
  Example(false);
  return 0;
}

This example demonstrates a function GetInt that returns std::expected<int, Error>. If want_success is true, it returns a successful std::expected holding the value 42. Otherwise, it returns an std::unexpected containing an Error::Unavailable enum value. The Example function then checks if the result has_value() to determine success or failure, accessing the value or error accordingly. In real-world applications, the Error type would likely be more sophisticated, potentially holding error codes, messages, or even structured error information, similar to the richer error handling capabilities of absl::Status.

Problem #2: Constructor Error Handling and RAII

The c++ google style guide‘s avoidance of exceptions extends to constructors. Constructors, by design, do not have return values and cannot directly signal errors using exceptions. A common pattern to address this, and recommended by Abseil ToTW #42, is to employ a factory function and make the constructor private. This approach ensures that object creation can be controlled and error conditions can be returned explicitly.

#include <expected>
#include <iostream>
#include <memory>

enum class Error { InvalidArgument, Unavailable };

std::string ToString(const Error& e) {
  if (e == Error::InvalidArgument) {
    return "InvalidArgument";
  }
  if (e == Error::Unavailable) {
    return "Unavailable";
  }
  return "Unknown";
}

class Foo {
 public:
  static std::expected<std::unique_ptr<Foo>, Error> Create(bool want_success) {
    // Acquiring resources.
    if (want_success) {
      return std::unique_ptr<Foo>(new Foo());
    }
    return std::unexpected(Error::Unavailable);
  }

  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;
  ~Foo() {
    // Possibly cleanup, since it's RAII.
    resource_ = 0;
  }

  std::string Name() { return "Foo " + std::to_string(resource_); }

 private:
  Foo() : resource_(42) {}
  int resource_ = 0;
};

void Example(bool want_success) {
  auto foo = Foo::Create(want_success);
  if (foo.has_value()) {
    std::cout << "Got " << foo.value()->Name() << std::endl;
  } else {
    std::cout << "Got " << ToString(foo.error()) << std::endl;
  }
}

int main(int argc, char* argv[]) {
  Example(true);
  Example(false);
  return 0;
}

In this example, the Foo class has a private constructor and a static factory function Create. Create returns std::expected<std::unique_ptr<Foo>, Error>. On success, it returns a std::unique_ptr to a newly created Foo object; on failure, it returns an std::unexpected with an error. The use of std::unique_ptr ensures proper resource management and adheres to RAII principles, even when object creation might fail. While accessing members of Foo requires dereferencing the unique_ptr (e.g., foo.value()->Name()), this approach provides robust error handling during object construction, consistent with the c++ google style guide.

Risky Workaround: Move Semantics Considerations

While returning std::expected<std::unique_ptr<Foo>, Error> is the safest and most explicit approach, there are alternative techniques, albeit with increased complexity and risk. One such approach involves leveraging move semantics to return a Foo object directly from the factory function. However, this requires careful implementation of move constructors, move assignment operators, and destructors to ensure correct resource management. Mistakes in these areas can lead to subtle and difficult-to-debug resource leaks or double-free errors. Therefore, while technically feasible, directly returning objects using move semantics from factory functions for error-prone construction is generally less recommended than the std::unique_ptr approach, especially when prioritizing adherence to robust practices emphasized by the c++ google style guide.

Problem #3: Polymorphism and RAII with Factory Functions

Extending the factory function pattern to polymorphic hierarchies introduces another layer of complexity. If we have an abstract base class AbstractFoo and concrete derived classes like Foo, we might want a factory function to create instances of these derived classes and return them as AbstractFoo pointers. Directly using std::expected<AbstractFoo*, Error> is not possible as std::expected requires the contained type to be non-abstract and copyable (or movable).

A workable solution involves an intermediary “wrapper” class, ConcreteFoo, that holds a std::unique_ptr<AbstractFoo>. This wrapper class effectively manages the lifetime of the polymorphic object and allows us to return it within std::expected.

#include <expected>
#include <iostream>
#include <memory>

enum class Error { InvalidArgument, Unavailable };

std::string ToString(const Error& e) {
  if (e == Error::InvalidArgument) {
    return "InvalidArgument";
  }
  if (e == Error::Unavailable) {
    return "Unavailable";
  }
  return "Unknown";
}

class AbstractFoo {
 public:
  virtual std::string Name() = 0;
};

class Foo : public AbstractFoo {
 public:
  static std::expected<std::unique_ptr<Foo>, Error> Create(bool want_success) {
    // Acquiring resources.
    if (want_success) {
      return std::unique_ptr<Foo>(new Foo());
    }
    return std::unexpected(Error::Unavailable);
  }

  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;
  Foo(Foo&&) = default; // Allows returning Foo rather than
                         // std::unique_ptr<Foo>.
  ~Foo() {
    // Possibly cleanup, since it's RAII.
    resource_ = 0;
  }

  std::string Name() override { return "Foo " + std::to_string(resource_); }

 private:
  Foo() : resource_(42) {}
  int resource_ = 0;
};

class ConcreteFoo : public AbstractFoo {
 public:
  template <typename T>
  ConcreteFoo(std::unique_ptr<T> a_foo) : a_foo_(std::move(a_foo)) {}

  std::string Name() override { return a_foo_->Name(); }

 private:
  std::unique_ptr<AbstractFoo> a_foo_;
};

std::expected<ConcreteFoo, Error> CreatePolymorphicFoo(bool want_success) {
  return Foo::Create(want_success);
}

void Example(bool want_success) {
  auto foo = CreatePolymorphicFoo(want_success);
  if (foo.has_value()) {
    std::cout << "Got " << foo.value().Name() << std::endl;
  } else {
    std::cout << "Got " << ToString(foo.error()) << std::endl;
  }
}

int main(int argc, char* argv[]) {
  Example(true);
  Example(false);
  return 0;
}

The CreatePolymorphicFoo function now returns std::expected<ConcreteFoo, Error>. Inside ConcreteFoo, a std::unique_ptr<AbstractFoo> a_foo_ manages the actual polymorphic object. The constructor of ConcreteFoo is templated to accept std::unique_ptr of various derived types, as long as they are convertible to AbstractFoo. This approach allows us to use factory functions for polymorphic objects while still adhering to RAII and non-exception-based error handling principles advocated by the c++ google style guide. While ConcreteFoo adds an extra layer of indirection, it provides a clean way to manage polymorphic object lifetimes and error reporting within the constraints of std::expected.

Problem #4: Text Formatting Alternatives

The original post mentions issues with std::format availability. While std::format is the standard library’s modern formatting solution, fmt::format (from the fmt library) is a widely used and highly performant alternative, often serving as the basis for std::format. fmt::format generally offers excellent compatibility and features and is a suitable replacement when std::format is not readily available in your toolchain. For projects targeting broader compiler compatibility or requiring advanced formatting capabilities, fmt::format is a robust choice, aligning with the spirit of using well-established and efficient libraries in line with good c++ google style guide practices.

Problem #5: RAII Wrappers for C-style APIs

Interfacing with C-style APIs, like SDL2 mentioned in the original article, often requires manual resource management. C APIs typically rely on explicit allocation and deallocation functions (e.g., SDL_CreateTexture and SDL_DestroyTexture). To bring RAII principles to these APIs, we can create C++ wrappers using std::unique_ptr with custom deleters.

namespace sdl {
struct TextureDeleter {
  void operator()(SDL_Texture* tex) { SDL_DestroyTexture(tex); }
};
using Texture = std::unique_ptr<SDL_Texture, TextureDeleter>;
}  // namespace sdl

This code snippet demonstrates how to create an RAII wrapper sdl::Texture for SDL_Texture*. TextureDeleter is a custom deleter struct that defines the action to be performed when the std::unique_ptr goes out of scope – in this case, calling SDL_DestroyTexture. Using sdl::Texture ensures that SDL textures are automatically destroyed when they are no longer needed, preventing resource leaks and simplifying resource management when working with C-style APIs within a c++ google style guide compliant C++ codebase.

sdl::Texture texture(SDL_CreateTexture(renderer, ...));
if (texture == nullptr) {
  // error handling here
}

This instantiation shows how easily the wrapper can be used. The sdl::Texture object takes ownership of the SDL_Texture* returned by SDL_CreateTexture, and the custom deleter will handle cleanup automatically.

Conclusion: Practical C++ Error Handling and RAII

This exploration demonstrates practical approaches to error handling and RAII in modern C++ (C++23), consciously aligning with the principles of the c++ google style guide. While Abseil provides excellent tools, standard library features like std::expected and std::unique_ptr with custom deleters, combined with factory function patterns, offer viable and effective alternatives. These techniques enable developers to write robust, maintainable, and exception-free C++ code, even when faced with complex object initialization, polymorphism, and interactions with C-style APIs. By embracing these patterns, we can build upon the solid foundation of the c++ google style guide and leverage modern C++ features for safer and more predictable software development.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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