如何避免对具有组成关系的对象进行nullcheck

时间:2019-01-09 03:04:19

标签: c++

首先,我可能以错误的方式写下了英语,因为它不是第一语言。对此感到抱歉。

无论如何,现在我面临多次检查null的问题,因为我愿意对待的对象有很多对象。 让我给你看一个例子。

// def:
class A {
public:
    B* getB() { return _b; }
    D* getD() { return _d; }

private:
    B* _b;  
};

class B {
public:
    C* getC() { return _c; } // Train Wreck. but it's needed on my frw...
   ...
};

// usg:
void foo(A* got)
{
    if(!got)
         return;
    B* b = got->getB();
    if(!b)
         return;
    C* c = b->getC();
    if(!c)
         return;
    // do something...
}

如您所见,每个尝试访问成员数据的行上都有3行。 当然,我可以放一个宏来减少这些行。

#define WRD_GET_3(expr, res, ret) \
        _TGet<decltype(expr)>::set(expr);\
        WRD_IS_NULL_3(_TGet<decltype(expr)>::get(), res, ret)
#define WRD_GET_2(expr, ret) WRD_GET_3(expr, WRD_SPACE, ret)
#define WRD_GET_1(expr)  WRD_GET_2(expr, WRD_SPACE)
#define WRD_GET(...)     WRD_OVERLOAD(WRD_GET, __VA_ARGS__)

#define WRD_IS_NULL_3(VALUE, RES, RET)  \
    if((VALUE).isNull()) {              \
        RES.warn(#VALUE);               \
        return RET;                     \
    }

template <typename T>
class _TGet {
public:
    static T& set(T& expr) { return *store() = &expr, get(); }
    static T& get() { return **store(); }
    static T** store() {
        static T* inner = 0;
        return &inner;
    }
};


void foo(A* got)
{
    if(!got) return;
    B* b = WRD_GET(got->getB())
    C* c = WRD_GET(b->getC())
    if(!c) return;

    // do something...
}

但是,它仍然需要放一行来访问子obj。 有什么办法可以像下面这样吗?

void foo(A* got)
{
    C* c = got->getB()->getC();
    // if got or got->getB() is nullptr, c will got assigned to nullptr.
    // without exception.

    // do something...
}

听说过沉船的anipatt。但要坚持当前的班级设计。

谢谢。

C.F,我以为第一个溶胶就是在每种吸气剂方法上都加上“ if stmt”。

B* A::getB() {
    if(!this) return nullptr;
}

并且它在大多数情况下都有效,但不适用于虚拟方法。 (当然,因为有vtable)。

编辑: 很抱歉为它留下了味精。 即使我使用ref代替,仍然有一些时间检查指针我持有什么。 而且我之前曾提出过例外的想法,但担心这样做对我来说太昂贵了。 最好是使用nullobject或我的宏来解决。

感谢您的所有答复。



编辑2: 由于某种原因,我回到了这个任务,并通过重用上面编写的WRD_GET宏为我想做的事情创建了一个宏。

#define _PUT(exp) _TGet<TypeTrait<decltype(exp)>::Org>::set(exp)
#define _GET(exp) _TGet<TypeTrait<decltype(exp)>::Org>::get()
#define _NULR(exp) nulr<TypeTrait<decltype(exp)>::Org>()
#define WRD_GET_1(e1) _PUT(e1)
#define WRD_GET_2(e1, e2) WRD_GET_1(e1).isNull() ? _NULR(e1.e2) : _PUT(e1.e2)
#define WRD_GET_3(e1, e2, e3) _PUT(e1).isNull() ? _NULR(e1.e2.e3) : (_PUT(e1.e2).isNull() ? _NULR(e1.e2.e3) : _PUT(e1.e2.e3))
#define WRD_GET_4(e1, e2, e3, e4) _PUT(e1).isNull() ? _NULR(e1.e2.e3.e4) : (_PUT(e1.e2).isNull() ? _NULR(e1.e2.e3.e4) : (_PUT(e1.e2.e3).isNull() ? _NULR(e1.e2.e3.e4) : _PUT(e1.e2.e3.e4)))
#define WRD_GET(...)            WRD_OVERLOAD(WRD_GET, __VA_ARGS__)
// yes... it's looks dumb.

用法

// as-is:
A* a = getA();
if(!a) return;
B* b = a.getB();
if(!b) return;
C* c = b.getC();
if(!c) return;

// to-be:
C* c = WRD_GET(getA(), getB(), getC()); // when I can recognize those commas into dots, It's pretty intuitive to me.

WRD_GET已被设计来避免重复通话问题。 这次,新的WRD_GET宏通过获取对您给出的空传播页的提示而扩展为接受多个访问。谢谢。

无论如何,即使它可以避免重复调用,它仍然需要放置和调用其他g / setter,并且可能会稍微降低性能。和代码可以通过像FOR宏来改进。 但是,这对我的环境很有效。

1 个答案:

答案 0 :(得分:2)

实际上,this指针不能为空(如果在指定this时引用已经存在,则将是一个引用),因此if检查无效:

B* A::getB()
{
    if(!this) return nullptr;
}

因为访问没有静态对象的非静态成员函数是未定义的行为

A* a = nullptr;
a->getB();     // UB/illegal, whether you have the check inside getB or not!!!

如果要避免检查空指针,请使用引用:

foo(A& a)
{
   B& b = a.getB();
}

使用

B& A::getB() { return *_b; }
// but you need to guarantee that _b is never a null pointer!
// or you make, if it is never changed anyway, _b a reference as well

如果不能保证_b存在,则可以抛出:

B& A::getB()
{
    if(_b)
        return *_b;
    throw std::runtime_error("illegal state");
}

这样,如果函数返回,则可以确保获得有效的引用。但是,这种方法会迫使您在其他时候捕获异常:

B* b = a.getB(); // assuming a being a reference already...
if(b)
{
    // use b
}
else
{
    // handle b not existing
}

vs。

try
{
    B& b = a.getB();
    // use b
}
catch(std::runtime_error const&)
{
    // handle b not existing
}

当然,您不需要将try / catch放在每个访问器的周围,但是可以将它放在更大的块周围:

try
{
    B& b = a.getB();
    C& c = b.getC();
    D& d = c.getD();
    // use d

    // even this is valid:
    D& dd = a.getB().getC().getD();
    // as the exception will prevent the next function calls in the chain
}
catch(std::runtime_error const&)
{
}

您甚至可以让异常进一步传播,并仅在以后(在foo之外)捕获它。

至少,您应该将它捕获到main中,以确保所调用堆栈上的对象的析构函数(从而使RAII正常工作)。

请注意,更改了foo签名后,您可能需要检查foo外的指针:

A a;
foo(a); // fine; especially: no check that is not needed

A* p = ...;
if(p)        // needed unless that you are 100% sure that the pointer is valid
    foo(*p);

但是,在某些系统上,异常处理可能会很昂贵。如果A无法提供B对象是一种非常正常的情况,则您可能希望保留显式检查指针的方法。