【问题标题】:Nullable values in C++C++ 中的可空值
【发布时间】:2011-02-02 00:51:52
【问题描述】:

我正在用原生 C++ 创建一个数据库访问层,并且正在寻找支持 NULL 值的方法。这是我目前所拥有的:

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;
};

这就是我必须定义函数的方式:

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

我这样称呼它:

sp.StoredProc(1, 2);

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

我只是想知道是否有比这更好的方法。特别是我不喜欢带有静态的 CNullValue 的单例对象。 我宁愿做

sp.StoredProc(3, CNullValue);

或类似的东西。其他人是如何解决这个问题的?

【问题讨论】:

    标签: c++ data-access-layer nullable


    【解决方案1】:

    Boost.Optional 可能满足您的需求。

    boost::none 代替您的CNullValue::Null()。由于它是一个值而不是成员函数调用,因此为简洁起见,如果您愿意,可以使用 using boost::none;。它转换为bool 而不是IsNull,和operator* 而不是GetValue,所以你可以这样做:

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

    但我认为,您提出的设计本质上是相同的。

    【讨论】:

    • 特别是考虑到它在性能方面等同于嵌入值,因为它们不使用堆分配。
    【解决方案2】:

    编辑:改进了“null”值抛出异常。更多修复

    如果Boost.Optionalstd::optional 不是一个选项,在c++11 中,您还可以利用nullptrnullptr_t typedef 创建一个Nullable&lt;T&gt;,其语义与.NET 几乎相同一个。

    #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://stackoverflow.com/questions/42183631/inability-to-overload-dot-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;
    }
    

    我在 gcc、clang 和 VS15 中对其进行了测试:

    #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;
    }
    

    【讨论】:

    • 其中一个 != 重载中有错字:“if (!op.Value.true)”
    • @AaronHS 谢谢。修复并添加了更多有用的语义(方法 TryGet)
    • 很好,但我得到了各种编译错误,例如 Error C2248 'CNullable::NullableValue::NullableValue': cannot access private member declaration in class 'CNullable ::NullableValue'。 (请注意,我将课程重命名为 CNullable&lt;&gt;
    • @JonathanWood 当然对我有用。确保您还重命名了类朋友指令
    【解决方案3】:

    C++ 有很多 Nullable 类型的实现,而且大多数都不完整。在 C++ 世界中,可空类型被称为可选类型。这是为 C++14 提出的,但被推迟了。然而,实现它的代码可以在大多数 C++11 编译器上编译和工作。您可以只放入实现可选类型的单个头文件并开始使用它:

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

    示例用法:

    #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';
    }
    

    更多文档:http://en.cppreference.com/w/cpp/experimental/optional

    另见我的其他回答:https://stackoverflow.com/a/37624595/207661

    【讨论】:

    • 我在答案中的实现很紧凑,并且支持我在 .net 版本中可以识别的所有语义。
    • 这应该是公认的答案,标准库解决方案。支持 C++17 的编译器已经支持它。
    【解决方案4】:

    IsNull 替换为HasValue,您就拥有了.NET Nullable 类型。

    当然……这是 C++。为什么不直接使用指向“原始”类型的指针?

    【讨论】:

    • 指针有不同的复制语义。如果你复制一个非空指针,那么这个副本指向的是同一个对象。如果您复制此 CNullableT,则该副本具有其自己的值实例。在某些情况下,您需要一个,而在某些情况下需要另一个,但这与您是否希望值范围为“任何 T​​ 或无”是一个独立的问题。因此,将指针用于可选值会带来一些负担。
    • 我只是想让界面在大多数情况下更干净,所以我可以使用 StoredProcedcure(1, 2) 而不是 int p = 2;存储过程(1,&p);是的,.NET Nullable 类型被用作灵感:)
    • 比指针更好的是使用智能指针。如果不允许复制则为 std::unique_ptr,如果需要复制则为 std::shared_ptr。那么你就没有重复指向一个对象的问题了,你也不用担心内存泄漏。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多