C++中与C#的null合并运算符等效的是什么?

32

是否有C++等效的C#空合并运算符? 我在我的代码中进行了太多的空值检查。 因此,正在寻找一种减少空代码量的方法。


12
在GCC中,使用a ?: b可以实现相同的功能,但这种写法不具备可移植性。 - MSalters
@MSalters:这不完全正确,因为如果 false ?: 123123,而 coalesce(false, 123)false - einpoklum
@einpoklum:你似乎对“合并”有一个特定的定义。例如,SQL中的COALESCE(NULL, 123)123。我的非正式定义是“列表中第一个真实值”。 - MSalters
@MSalters,这不是我,而是 OP 的问题... 这个问题涉及 C# 的 coalesce。我刚刚发布了一个答案,请在那里查看详细信息。 - einpoklum
9个回答

18

1
链接已经失效,导致出现了404错误。 - TobiMcNamobi
2
新链接:https://gcc.gnu.org/onlinedocs/gcc-5.3.0/gcc/Conditionals.html#Conditionals 简而言之,GNU扩展允许您省略三元运算符的中间部分,并且在该条件为“非零”时,该条件成为返回值。 - Cory-G
1
这是不正确的,因为 false ?: 123123,而 coalesce(false, 123) 将会是 false - einpoklum

13

在C++中默认情况下没有这样的方法,但是你可以自己编写一个:

在C#中,??运算符被定义为

a ?? b === (a != null ? a : b)

那么,C++方法看起来会像这样

Coalesce(a, b) // put your own types in, or make a template
{
    return a != null ? a : b;
}

25
即使a不为空,此方法也会评估b。如果b具有副作用,则可能会出现问题。 - Amnon
4
@Amnon:确实。请考虑以下代码: p = Coalesce(p, new int(10));。在C#中,似乎右操作数不能为NULL(无法在编译时检查,因为C++没有可空类型?)。另一方面,在C++中是否真的有这么大的需求,以至于你不能只键入 a ? a : b;吗? - UncleBens
5
如果使用的是C++11或更新版本,应该使用nullptr。无论如何在C/C++中都没有null,只有某些头文件中定义的NULL。https://dev59.com/b3M_5IYBdhLWcg3wq1CF - Csaba Toth
1
@Lunyx:在A??B??C的情况下,B??C是第一个??的右操作数。换句话说,它相当于A??(B??C)。此外,没有限制右操作数不能为空,只是会导致整个表达式为空。 - Cemafor
1
抱歉,我的错,你是正确的。由于“a”和“b”是局部变量,所以这段代码是线程安全的。 - M.kazem Akhgary
显示剩余7条评论

5

使用模板和C++11的lambda表达式:

template<typename TValue, typename TRhsEvaluator>
TValue coalesce(TValue lhsValue, TRhsEvaluator evaluateRhs) {
     return lhsValue ? lhsValue : evaluateRhs();
}
  • 第一个参数(左操作数)只会被评估一次。
  • 如果第一个参数为false,则仅评估第二个参数(右操作数)。

请注意,if?将提供的表达式静态转换为bool,而指针具有内置的explicit operator bool() const运算符,它等同于!= nullptr

示例用法:

void * const      nonZeroPtr = reinterpret_cast<void *>(0xF);
void * const otherNonZeroPtr = reinterpret_cast<void *>(0xA);

std::cout << coalesce(nonZeroPtr, [&] () {
    std::cout << "Side-effect. Should never be printed" << std::endl;
    return otherNonZeroPtr;
}) << std::endl;

上面的代码只会在控制台打印 0xf

右侧需要包装在一个 lambda 内 - 我们无法避免这种样板。实际上,语言应该原生提供空值合并运算符。


4
这是仅有的一个提供空值合并运算符相同语义的答案。尽管这样做可能很少值得麻烦,但实际上这个特性只需要在语言中存在就可以了。 - Edward Brey

3
这里有两个宏来复制 ?? ?.运算符。这些宏确保:
  • 参数只被评估一次。
  • 只有在第一个参数为空时才会评估第二个参数。
  • 通过编译时移除lambda语法,宏变量使用内部作用域,并避免了宏变量冲突。
例如用法:
    COA( nullPtr, goodPtr )->sayHello();
    COA( nullPtr, COA( nullPtr, goodPtr ) )->sayHello();

    COACALL( goodPtr, sayHello() );
    COACALL( nullPtr, sayHello() );

    COACALL( COA( nullPtr, goodPtr ), sayHello() );

定义:

    #define COA(a, b) ([&](){ auto val = (a); return ((val) == NULL ? (b) : (val)); }())
    #define COACALL(a, b) ([&](){ auto val = (a); if (val) (val->b); }());

注意: COACALL 不返回结果。只能用于无返回值的调用或根据需要进行修改。

这个 COALESCE 实现简单且避免了其他答案的缺点。应该被评价得更高。 - zzz

3

我想要扩展@Samuel Garcia的答案,泛化模板并添加帮助器宏来减少lambda样板代码:

#include <utility>

namespace coalesce_impl
{
    template<typename LHS, typename RHS>
    auto coalesce(LHS lhs, RHS rhs) ->
        typename std::remove_reference<decltype(lhs())>::type&&
    {
        auto&& initialValue = lhs();
        if (initialValue)
            return std::move(initialValue);
        else
            return std::move(rhs());
    }

    template<typename LHS, typename RHS, typename ...RHSs>
    auto coalesce(LHS lhs, RHS rhs, RHSs ...rhss) ->
        typename std::remove_reference<decltype(lhs())>::type&&
    {
        auto&& initialValue = lhs();
        if (initialValue)
            return std::move(initialValue);
        else
            return std::move(coalesce(rhs, rhss...));
    }
}

#define COALESCE(x) (::coalesce_impl::coalesce([&](){ return ( x ); }))
#define OR_ELSE     ); }, [&](){ return (

使用宏,您只需:
int* f();
int* g();
int* h();

int* x = COALESCE( f() OR_ELSE g() OR_ELSE h() );

我希望这能有所帮助。

2
有一个GNU GCC扩展允许在中间操作数缺失的情况下使用?:运算符,请参见Conditionals with Omitted Operands

条件表达式中的中间操作数可以省略。如果第一个操作数非零,则其值为条件表达式的值。

因此,表达式

x ? : y

如果x非零,则其值为x;否则,其值为y

这个例子与

x ? x : y

等效。

在这种简单情况下,省略中间操作数的能力并不是特别有用。当第一个操作数包含副作用时(如果它是宏参数,则可能会包含副作用),重复中间操作数将执行两次副作用。省略中间操作数使用已经计算出的值,而不会重新计算它带来的不良影响。

这个扩展也被clang支持。但是,在使用扩展之前,您应该检查您正在使用的编译器和代码的可移植性要求。值得注意的是,MSVC C++编译器不支持?:中省略的操作数。

另请参见相关的StackOverflow讨论here


1

这个怎么样?

#define IFNULL(a,b) ((a) == null ? (b) : (a))

3
当使用IFNULL(a++, b)时,请注意a可能为null。此外,IFNULL(SomeClass.DoSomethingReallyLong(), "")也会引起问题。 - McKay
1
好观点——我懒惰的下场就是这样。在这里使用模板方法绝对是正确的选择,因为它避免了副作用并且性能相当(甚至更好)。现在我要在@McKay的解决方案上加上一个+1了。 :-) - Justin Grant
4
函数模板的性能不一定比普通函数更好。这个函数模板会进行“短路运算”(只有在a为空时才不执行b),而函数的参数总是需要被计算的。 - Steve Jessop
2
也是一个好观点。但是,我认为最常见的用例(至少在我使用 ?? 运算符时)是当 a 比 b 更昂贵(例如数据库调用、XML 解析)时。通常情况下,b 是一个常量,比如 0 或 ""。因此,我怀疑模板大多数时间会胜出。此外,除非您知道宏的实现方式,否则双重执行对于宏的调用者来说并不明显,而函数调用语义是众所周知的,因此任何具有昂贵 b 的人都知道跳过函数,只需手动创建临时变量即可。有趣的是,我正在反驳自己的答案... :-) - Justin Grant
2
你可以用lambda来包装,这样a就不会被评估两次了。#define IFNULL(a, b) ([&](){ auto val = (a); return ((val) == NULL ? (b) : (val)); }()) - Cory-G
显示剩余3条评论

0
仅仅是为了补充一下提到 ?: 操作符(也称为“Elvis操作符”)的答案:我有时会使用一个辅助函数和这个操作符一起,该函数获取指针或std::optional或类似类型的基础值,这些类型“包装”一个值并具有布尔转换以指示值的存在。例如:
template <typename T>
constexpr T coalesce (std::optional<T> opt) {
    return *opt;
}
template <typename T>
constexpr T coalesce (T fallback) {
    return fallback;
}

std::optional<int> opt1{5};
std::optional<int> opt2;
int val1 = coalesce(opt1 ?: 0);
int val2 = coalesce(opt2 ?: 0);

唯一的缺点是必须小心使用,不能静态检查正确使用。例如,您可以只执行coalesce(opt2)而不带?:fallback ,这与在首次检查其是否包含任何内容之前执行* opt2 相同。因此,名称 coalesce 有点误导人,但在正确使用时,在我看来它看起来很自我解释(而且非常整洁)。

0

提醒:C#的空值合并语义

??,C#中的空值合并运算符的语义

p ?? q

Here, p is the left and q is the right operand of [the] ?? operator. The value of p can be nullable type, but the value of q must be non-nullable type. If the value of p is null, then it returns the value of q. Otherwise, it will return the value of p.

C++ 实现

首先我们注意到 C++ 不能使用 ?? 作为标识符;预处理器也不允许我们定义一个这个名称的宏。因此,我们可以使用 coalesce 作为标识符。

我们需要以请求的语义定义 coalesce 为中缀运算符。这可以通过运算符重载和宏的组合来实现,使我们可以编写:

int* p = // whatever
int* q = // whatever
int* result = p coalesce q;

这是一个实现:

#include <functional>

namespace detail {

struct coalesce_op {};

template <typename T>
struct coalesce_op_primed { 
    T&& lhs; 

    constexpr T&& operator+(T&& rhs) {
        return (lhs == nullptr) ? rhs : lhs;
    }
};

template <typename T>
constexpr coalesce_op_primed<T> operator+(T&& t, coalesce_op)
{
    return coalesce_op_primed<T>{std::forward<T>(t)};
}

} // namespace detail

#define coalesce + detail::coalesce_op{} +

GodBolt上看它的实际应用。

注意事项:

  • 没有短路逻辑,即使未使用,也会评估q。可以编写具有短路逻辑的宏,但它不会成为中缀运算符。
  • 我们不能使用GNU C扩展的?:Elvis运算符,因为其语义不同。它将考虑非空false值作为选择RHS操作数的原因,即false?:123将产生123,而false ?? 123使用C#合并语义是false
  • 我没有尝试使其适用于std::optional,检测nullopt。如果需要,也应该可以通过一些if-constexpr或tagged-dispatch TMP实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接