Nullable values in C++
Asked Answered
C

4

36

I'm creating a database access layer in native C++, and I'm looking at ways to support NULL values. Here is what I have so far:

class CNullValue
{
public:
    static CNullValue Null()
    {
        static CNullValue nv;

        return nv;
    }
};

template<class T>
class CNullableT
{
public:
    CNullableT(CNullValue &v) : m_Value(T()), m_IsNull(true)
    {
    }

    CNullableT(T value) : m_Value(value), m_IsNull(false)
    {
    }

    bool IsNull()
    {
        return m_IsNull;
    }

    T GetValue()
    {
        return m_Value;
    }

private:
    T m_Value;
    bool m_IsNull;
};

This is how I'll have to define functions:

void StoredProc(int i, CNullableT<int> j)
{
    ...connect to database
    ...if j.IsNull pass null to database etc
}

And I call it like this:

sp.StoredProc(1, 2);

or

sp.StoredProc(3, CNullValue::Null());

I was just wondering if there was a better way than this. In particular I don't like the singleton-like object of CNullValue with the statics. I'd prefer to just do

sp.StoredProc(3, CNullValue);

or something similar. How do others solve this problem?

Cancel answered 29/3, 2010 at 12:50 Comment(0)
I
36

Boost.Optional probably does what you need.

boost::none takes the place of your CNullValue::Null(). Since it's a value rather than a member function call, you can do using boost::none; if you like, for brevity. It has a conversion to bool instead of IsNull, and operator* instead of GetValue, so you'd do:

void writeToDB(boost::optional<int> optional_int) {
    if (optional_int) {
        pass *optional_int to database;
    } else {
        pass null to database;
    }
}

But what you've come up with is essentially the same design, I think.

Ireneirenic answered 29/3, 2010 at 12:56 Comment(1)
Especially considering that it's equivalent to the embedded value performance-wise as they do not use heap-allocation.Ruddle
F
22

EDIT1: Improved with throw exception on "null" Value. More fixes

EDIT2: An alternative version supporting nullable references can be found here.

If Boost.Optional or std::optional are not an option, in c++11 you can also take advantage of nullptr and the nullptr_t typedef to create a Nullable<T> with pretty much same semantics as .NET one.

#pragma once

#include <cstddef>
#include <stdexcept>

template <typename T>
class Nullable final
{
public:
    Nullable();
    Nullable(const T &value);
    Nullable(std::nullptr_t nullpointer);
    const Nullable<T> & operator=(const Nullable<T> &value);
    const Nullable<T> & operator=(const T &value);
    const Nullable<T> & operator=(std::nullptr_t nullpointer);
    bool HasValue() const;
    const T & GetValueOrDefault() const;
    const T & GetValueOrDefault(const T &def) const;
    bool TryGetValue(T &value) const;
    T * operator->();
    const T * operator->() const;
    T & operator*();
    const T & operator*() const;

public:
    class NullableValue final
    {
    public:
        friend class Nullable;

    private:
        NullableValue();
        NullableValue(const T &value);

    public:
        NullableValue & operator=(const NullableValue &) = delete;
        operator const T &() const;
        const T & operator*() const;
        const T * operator&() const;

        // https://mcmap.net/q/324392/-inability-to-overload-dot-39-39-operator-in-c
        const T * operator->() const;

    public:
        template <typename T2>
        friend bool operator==(const Nullable<T2> &op1, const Nullable<T2> &op2);

        template <typename T2>
        friend bool operator==(const Nullable<T2> &op, const T2 &value);

        template <typename T2>
        friend bool operator==(const T2 &value, const Nullable<T2> &op);

        template <typename T2>
        friend bool operator==(const Nullable<T2> &op, std::nullptr_t nullpointer);

        template <typename T2>
        friend bool operator!=(const Nullable<T2> &op1, const Nullable<T2> &op2);

        template <typename T2>
        friend bool operator!=(const Nullable<T2> &op, const T2 &value);

        template <typename T2>
        friend bool operator!=(const T2 &value, const Nullable<T2> &op);

        template <typename T2>
        friend bool operator==(std::nullptr_t nullpointer, const Nullable<T2> &op);

        template <typename T2>
        friend bool operator!=(const Nullable<T2> &op, std::nullptr_t nullpointer);

        template <typename T2>
        friend bool operator!=(std::nullptr_t nullpointer, const Nullable<T2> &op);

    private:
        void checkHasValue() const;

    private:
        bool m_hasValue;
        T m_value;
    };

public:
    NullableValue Value;
};

template <typename T>
Nullable<T>::NullableValue::NullableValue()
    : m_hasValue(false), m_value(T()) { }

template <typename T>
Nullable<T>::NullableValue::NullableValue(const T &value)
    : m_hasValue(true), m_value(value) { }

template <typename T>
Nullable<T>::NullableValue::operator const T &() const
{
    checkHasValue();
    return m_value;
}

template <typename T>
const T & Nullable<T>::NullableValue::operator*() const
{
    checkHasValue();
    return m_value;
}

template <typename T>
const T * Nullable<T>::NullableValue::operator&() const
{
    checkHasValue();
    return &m_value;
}

template <typename T>
const T * Nullable<T>::NullableValue::operator->() const
{
    checkHasValue();
    return &m_value;
}

template <typename T>
void Nullable<T>::NullableValue::checkHasValue() const
{
    if (!m_hasValue)
        throw std::runtime_error("Nullable object must have a value");
}

template <typename T>
bool Nullable<T>::HasValue() const { return Value.m_hasValue; }

template <typename T>
const T & Nullable<T>::GetValueOrDefault() const
{
    return Value.m_value;
}

template <typename T>
const T & Nullable<T>::GetValueOrDefault(const T &def) const
{
    if (Value.m_hasValue)
        return Value.m_value;
    else
        return def;
}

template <typename T>
bool Nullable<T>::TryGetValue(T &value) const
{
    value = Value.m_value;
    return Value.m_hasValue;
}

template <typename T>
Nullable<T>::Nullable() { }

template <typename T>
Nullable<T>::Nullable(std::nullptr_t nullpointer) { (void)nullpointer; }

template <typename T>
Nullable<T>::Nullable(const T &value)
    : Value(value) { }

template <typename T2>
bool operator==(const Nullable<T2> &op1, const Nullable<T2> &op2)
{
    if (op1.Value.m_hasValue != op2.Value.m_hasValue)
        return false;

    if (op1.Value.m_hasValue)
        return op1.Value.m_value == op2.Value.m_value;
    else
        return true;
}

template <typename T2>
bool operator==(const Nullable<T2> &op, const T2 &value)
{
    if (!op.Value.m_hasValue)
        return false;

    return op.Value.m_value == value;
}

template <typename T2>
bool operator==(const T2 &value, const Nullable<T2> &op)
{
    if (!op.Value.m_hasValue)
        return false;

    return op.Value.m_value == value;
}

template <typename T2>
bool operator==(const Nullable<T2> &op, std::nullptr_t nullpointer)
{
    (void)nullpointer;
    return !op.Value.m_hasValue;
}

template <typename T2>
bool operator==(std::nullptr_t nullpointer, const Nullable<T2> &op)
{
    (void)nullpointer;
    return !op.Value.m_hasValue;
}

template <typename T2>
bool operator!=(const Nullable<T2> &op1, const Nullable<T2> &op2)
{
    if (op1.Value.m_hasValue != op2.Value.m_hasValue)
        return true;

    if (op1.Value.m_hasValue)
        return op1.Value.m_value != op2.Value.m_value;
    else
        return false;
}

template <typename T2>
bool operator!=(const Nullable<T2> &op, const T2 &value)
{
    if (!op.Value.m_hasValue)
        return true;

    return op.Value.m_value != value;
}

template <typename T2>
bool operator!=(const T2 &value, const Nullable<T2> &op)
{
    if (!op.Value.m_hasValue)
        return false;

    return op.Value.m_value != value;
}

template <typename T2>
bool operator!=(const Nullable<T2> &op, std::nullptr_t nullpointer)
{
    (void)nullpointer;
    return op.Value.m_hasValue;
}

template <typename T2>
bool operator!=(std::nullptr_t nullpointer, const Nullable<T2> &op)
{
    (void)nullpointer;
    return op.Value.m_hasValue;
}

template <typename T>
const Nullable<T> & Nullable<T>::operator=(const Nullable<T> &value)
{
    Value.m_hasValue = value.Value.m_hasValue;
    Value.m_value = value.Value.m_value;
    return *this;
}

template <typename T>
const Nullable<T> & Nullable<T>::operator=(const T &value)
{
    Value.m_hasValue = true;
    Value.m_value = value;
    return *this;
}

template <typename T>
const Nullable<T> & Nullable<T>::operator=(std::nullptr_t nullpointer)
{
    (void)nullpointer;
    Value.m_hasValue = false;
    Value.m_value = T();
    return *this;
}

template <typename T>
T * Nullable<T>::operator->()
{
    return &Value.m_value;
}

template <typename T>
const T * Nullable<T>::operator->() const
{
    return &Value.m_value;
}

template <typename T>
T & Nullable<T>::operator*()
{
    return Value.m_value;
}

template <typename T>
const T & Nullable<T>::operator*() const
{
    return Value.m_value;
}

I tested it in gcc, clang and VS15 with the following:

#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
  (void)argc;
  (void)argv;

    Nullable<int> ni1;
    Nullable<int> ni2 = nullptr;
    Nullable<int> ni3 = 3;
    Nullable<int> ni4 = 4;
    ni4 = nullptr;
    Nullable<int> ni5 = 5;
    Nullable<int> ni6;
    ni6 = ni3;
    Nullable<int> ni7(ni3);
    //Nullable<int> ni8 = NULL; // This is an error in gcc/clang but it's ok in VS12

    cout << (ni1 == nullptr ? "True" : "False") << endl; // True
    cout << (ni2 == nullptr ? "True" : "False") << endl; // True
    cout << (ni2 == 3 ? "True" : "False") << endl; // False
    cout << (ni2 == ni3 ? "True" : "False") << endl; // False
    cout << (ni3 == 3 ? "True" : "False") << endl; // True
    cout << (ni2 == ni4 ? "True" : "False") << endl; // True
    cout << (ni3 == ni5 ? "True" : "False") << endl; // False
    cout << (ni3 == ni6 ? "True" : "False") << endl; // True
    cout << (ni3 == ni7 ? "True" : "False") << endl; // True

    //cout << ni1 << endl; // Doesn't compile
    //cout << ni3 << endl; // Doesn't compile
    cout << ni3.Value << endl; // 3
    //cout << ni1.Value << endl; // Throw exception
    //cout << ni2.Value << endl; // Throw exception
    //ni3.Value = 2; // Doesn't compile
    cout << sizeof(ni1) << endl; // 8 on VS15

    return 0;
}
Fluky answered 2/3, 2015 at 14:0 Comment(4)
there is a typo in one of the != overloads: "if (!op.Value.true)"Marlonmarlow
@Marlonmarlow thank you. Fixed and also added more useful semantics (methods TryGet)Fluky
Nice, but I get all sorts of compile errors such as Error C2248 'CNullable<int>::NullableValue::NullableValue': cannot access private member declared in class 'CNullable<int>::NullableValue'. (Note that I renamed the class to CNullable<>.Estelleesten
@JonathanWood of course it works for me. Ensure you also renamed the class friend directiveFluky
B
5

There are lot of Nullable type implementation for C++ and most are incomplete. In C++ world, nullable types are called optional types. This was proposed for C++14 but got postponed. However the code to implement it compiles and works on most C++11 compilers. You can just drop in the single header file implementing optional type and start using it:

https://raw.githubusercontent.com/akrzemi1/Optional/master/optional.hpp

Sample usage:

#if (defined __cplusplus) && (__cplusplus >= 201700L)
#include <optional>
#else
#include "optional.hpp"
#endif

#include <iostream>

#if (defined __cplusplus) && (__cplusplus >= 201700L)
using std::optional;
#else
using std::experimental::optional;
#endif

int main()
{
    optional<int> o1,      // empty
                  o2 = 1,  // init from rvalue
                  o3 = o2; // copy-constructor

    if (!o1) {
        cout << "o1 has no value";
    } 

    std::cout << *o2 << ' ' << *o3 << ' ' << *o4 << '\n';
}

More documentation: http://en.cppreference.com/w/cpp/experimental/optional

Also see my other answer: https://mcmap.net/q/324393/-implementing-boost-optional-in-c-11

Bedfordshire answered 3/6, 2016 at 22:56 Comment(2)
My implementation in the answers is compact and support all the semantics I could identify in the .net version.Fluky
This should be the accepted answer, standard library solution. C++17 capable compilers already support it.Eppie
B
3

Replace IsNull with HasValue and you've got the .NET Nullable type.

Of course.. this is C++. Why not just use a pointer to a "primitive" type?

Bellwether answered 29/3, 2010 at 12:55 Comment(3)
Pointers have different copy semantics. If you copy a non-null pointer, then the copy refers to the same object. If you copy this CNullableT, the copy has its own instance of the value. In some situations you want one and in some the other, but that's an independent issue from whether you want the range of values to be "any T, or none". So using a pointer for an optional value brings some baggage.Ireneirenic
I was just trying to make the interface cleaner for the majority of cases, so I can do StoredProcedcure(1, 2) instead of int p = 2; StoredProcedcure(1, &p); Yes, the .NET Nullable type was used as inspiration :)Cancel
Better than a pointer would be to use a smart pointer. std::unique_ptr if copying is not to be allowed, and std::shared_ptr if copying is required. Then you have no problem of duplicate pointers to one object, and you don't have to worry about memory leaking.Debbiedebbra

© 2022 - 2024 — McMap. All rights reserved.