Enhance ETL With `not_null<T*>`: Feature Request Discussion
Hey everyone,
I wanted to discuss a feature request for the ETL library: the implementation of a not_null<T*>
type. I'm curious to hear your thoughts and see if there's broader interest in this. Let's dive into the details!
The Need for not_null<T*>
In modern C++ development, ensuring data integrity and preventing null pointer dereferences are crucial. The existing std::reference_wrapper<>
serves a purpose, but there are scenarios where a dedicated not_null<T*>
type could offer a more explicit and potentially cleaner solution. When dealing with dependencies injected into classes, for instance, it's common to guarantee that certain pointers should never be null. This is where not_null<T*>
could shine, enhancing code clarity and reducing potential errors.
Current Challenges with std::reference_wrapper<>
Currently, when injecting dependencies, you might use raw pointers to avoid linter warnings about non-copyable classes. This often leads to using a reference in the constructor and then storing the raw pointer internally. While this works, it lacks explicitness. You lose the guarantee at the type level that the pointer will never be null. std::reference_wrapper<>
is an alternative, but it requires using .get()
every time you want to access the underlying object, which can make the code a bit verbose. The std::reference_wrapper<>
can add some boilerplate. You have to call get()
every time you use the reference, which can clutter your code and make it less readable. Plus, it doesn't have the same level of compile-time safety as a dedicated not_null<T*>
type.
Proposed Solution: etl::not_null<T*>
The idea is to introduce etl::not_null<T*>
, a template that wraps a pointer and ensures it's never null. This would provide a clear, type-level guarantee that the pointer is always valid. This approach offers a more direct and expressive way to convey the non-nullable constraint. By using etl::not_null<T*>
, you explicitly state that a pointer should never be null, making your code more self-documenting and easier to understand. This can be particularly beneficial in large projects where clarity and maintainability are paramount. Consider the benefits of improved code readability and enhanced safety.
Example Scenario
Let’s consider a practical example. Imagine a class that holds a reference to a dependency. Currently, you might implement it like this:
template <Dependency>
class HoldsReference
{
public:
explicit HoldsReference(Dependency& dep) noexcept
: myDependency{&dep}
{
}
void usage() noexcept
{
myDependency->Call();
}
private:
// store a pointer so that this class doesn't trigger linter warnings about it being non-copyable/non-movable
Dependency* myDependency;
};
With etl::not_null<T*>
, the code could look like this:
template <Dependency>
class HoldsReference
{
public:
explicit HoldsReference(Dependency& dep) noexcept
: myDependency{dep}
{
}
void usage() noexcept
{
// same usage, `not_null` implements operator-> and operator* ?
myDependency->Call();
}
private:
// store a pointer so that this class doesn't trigger linter warnings about it being non-copyable/non-movable
etl::not_null<Dependency*> myDependency;
};
Notice how etl::not_null<Dependency*> myDependency;
clearly communicates that myDependency
will never be null. This improves readability and reduces the cognitive load for anyone reading the code. The explicitness of not_null<T*>
makes it clear that the pointer is guaranteed to be valid, which can help prevent errors and simplify debugging. Furthermore, the proposed not_null
would ideally implement operator->
and operator*
, ensuring seamless usage without the need for extra .get()
calls. This keeps the code clean and efficient, while maintaining the safety guarantees.
Advantages of not_null<T*>
- Explicitness: Clearly indicates that a pointer should never be null.
- Safety: Helps prevent null pointer dereferences.
- Readability: Makes code easier to understand and maintain.
- Seamless Usage: With
operator->
andoperator*
implemented, usage remains straightforward.
Potential Implementation Details
Ideally, etl::not_null<T*>
would:
- Throw an exception or assert in the constructor if a null pointer is passed.
- Implement
operator->
andoperator*
for easy access to the underlying object. - Be compatible with standard library algorithms and data structures.
How not_null<T*>
Enhances Code Safety
The primary goal of not_null<T*>
is to enhance code safety by preventing null pointer dereferences. In many applications, certain pointers are expected to always point to valid objects. Using raw pointers or even std::unique_ptr
or std::shared_ptr
doesn't provide a compile-time guarantee that the pointer is not null. This is where not_null<T*>
steps in. By wrapping a raw pointer, not_null<T*>
enforces the constraint that the pointer must never be null. The constructor of not_null<T*>
can check for null and throw an exception or assert if a null pointer is passed. This early detection of null pointers can prevent crashes and undefined behavior later in the program's execution. Moreover, the use of not_null<T*>
communicates the intent of the code more clearly. It tells other developers (and the compiler) that this pointer is expected to be valid at all times. This can help prevent accidental assignment of null to the pointer and make the code easier to reason about. The safety guarantees provided by not_null<T*>
can be particularly valuable in safety-critical applications or large codebases where it's essential to minimize the risk of null pointer dereferences.
Comparison with Smart Pointers
It's important to distinguish not_null<T*>
from smart pointers like std::unique_ptr
and std::shared_ptr
. Smart pointers are primarily designed for memory management, ensuring that dynamically allocated objects are properly deallocated when they are no longer needed. While smart pointers can also help prevent memory leaks, they don't inherently guarantee that the pointer is not null. A std::unique_ptr
or std::shared_ptr
can still be null, representing the absence of an object. not_null<T*>
has a different purpose. It's not about memory management; it's about enforcing a constraint that a pointer must always point to a valid object. It complements smart pointers by providing an additional layer of safety. You could, for example, use not_null<std::unique_ptr<T>>
to ensure that a unique pointer is both non-null and properly managed. In this scenario, std::unique_ptr
takes care of memory management, while not_null<T*>
ensures that the pointer is never null. This combination provides a robust solution for scenarios where both memory management and null pointer safety are important. By focusing solely on non-nullability, not_null<T*>
offers a specialized tool for a specific problem, making it a valuable addition to the C++ developer's toolkit. The key takeaway here is that not_null<T*>
enhances safety without replacing smart pointers.
Real-World Use Cases and Benefits
The benefits of not_null<T*>
extend across various real-world scenarios. Consider dependency injection, where components receive references to other components they depend on. In such cases, it's often critical that these dependencies are valid and not null. Using not_null<T*>
ensures that these dependencies are always available, preventing unexpected crashes or undefined behavior. Another use case is in data structures and algorithms. If you have a data structure that relies on certain pointers being valid, not_null<T*>
can enforce this constraint, making the data structure more robust. For example, in a graph data structure, you might use not_null<T*>
to ensure that edge pointers always point to valid nodes. In game development, where performance and reliability are crucial, not_null<T*>
can help prevent common bugs related to null pointer dereferences. Imagine a game engine where entities have components. If a component pointer is null, it could lead to a crash or unexpected behavior. By using not_null<T*>
, you can ensure that component pointers are always valid. In embedded systems, where resources are limited and crashes can have serious consequences, not_null<T*>
can improve the reliability of the system. By catching null pointer errors early, you can prevent system failures and ensure that the system operates correctly. Overall, not_null<T*>
enhances reliability, simplifies debugging, and improves collaboration.
Conclusion: Let's Discuss!
I believe etl::not_null<T*>
could be a valuable addition to the ETL library, offering a more explicit and safer way to handle non-nullable pointers. It addresses a specific need that isn't fully covered by existing tools like std::reference_wrapper<>
or smart pointers. Now, I'd love to hear your thoughts, guys. Do you see the value in this? Are there any potential drawbacks or alternative approaches we should consider? Let's discuss and explore this idea further! I'm eager to hear your perspectives and collectively decide if this is a worthwhile feature to pursue.