Dear All

 

This is an idea that I proposed some time ago, I have collected the feedbacks and made some change. It is about the function coalesce(T&& default_value, Args&&... to_test_v),  it looks for a not null value in to_test_v and If it does not find a not null value then coalesce returns default_value. It is similar to a coalesce functions implemented in different RDMS.

 

Some examples:

 

    int i = 5;

    int* pi = &i;

    std::optional<int> o = 12;

    std::unique_ptr<int> up = std::make_unique<int>(3);

    std::shared_ptr<int> sp = std::make_shared<int>(4);

    std::weak_ptr<int> wp = sp;

 

  the 1st parameter is the default value, the others are different type of pointers or std::optional

    const int r1 = coalesce(i,  pi, o, up, sp, wp);

    std::cout << r1 << std::endl;  // prints 5 because the 1st pointer not null is pi

 

 

 

Now if I define a function to calculate the default value

 

    int calc_default_value() noexcept

    {

        return 14;

    }

 

The default value can be a pointer to a function that will be called only if no null pointer has been found

    int r2 = coalesce(calc_default_value, nullptr);

    std::cout << r2 << std::endl;  // prints 14 the calculated default value

    

    int r2b = coalesce(calc_default_value, pi);

    std::cout << r2b << std::endl; // prints 5, the value pointed by pi

 

 

Other examples with more complex types

    std::vector<int> ints{ 0,1,2,3,4,5 };

    std::unique_ptr<std::vector<int>> upvn;

    const size_t r3 = coalesce(ints, upvn).size();

    std::cout << r3 << std::endl; // prints 6, the size of ints, the default value because upwn is null

 

    const std::vector<int>& v = coalesce(ints, upvn);

    std::cout << v.size() << std::endl; // prints 6, the size of ints, the default value because upwn is null

 

    std::unique_ptr<std::vector<int>> upv = std::make_unique<std::vector<int>>();

    const size_t r4 = coalesce(ints, upvn, upv).size();

    std::cout << r4 << std::endl; // prints 0, the size of upv

 

 

Let’s define a couple of other functions

   

    static int st = 123;

 

    int* calc_value() noexcept

    {

        return &st;

    }

 

    int* calc_value_null() noexcept

    {

        return nullptr;

    }

 

Not only the default value can be a pointer to a function, also any other parameter.

The functions will be called one after the other until an not null value is found.

    const int r5 = coalesce(10, calc_value_null, calc_value, pi);

    std::cout << r5 << std::endl; // prints 123, the value returned by calc_value

 

 

Also lambda can be passed

    auto l_null = []() noexcept -> int*

    {

        return nullptr;

    };

 

   

    const int r6 = coalesce(10, l_null, calc_value);

    std::cout << r6 << std::endl;  // prints 123, l_null returns nullptr, therefore calc_value is called, that returns 123

 

  

  Also std::function can be passed, in this case it will test also that the object of type std::function points to a function

    using fu = std::function<int* ()>;

    fu f = l_null;

      

    const int r7 = coalesce(2, f);

    std::cout << r7 << std::endl; // it prints 2, the default value because l_null, the function pointed by f, returns nullptr

 

    fu fn;

    const int r7b = coalesce(3, fn);

    std::cout << r7b << std::endl; // it prints 2, the default value because fn points to no function

 

 

Some other example of how coalesce can be used.

Let's say that we have a data feed that we want to process, and for each record we must use the 1st value not null, if there is

    std::vector<record> s1

    {

        {10, {}},

        {{}, 20},

        {{}, {}}

    };

   

    int r8 = std::accumulate(s1.begin(), s1.end(), 0,

        [](int i, const record& r) { return i + coalesce(0, r.v1, r.v2);  });

    std::cout << r8 << std::endl; // prints 30 = 10 + 20 + 0

 

    for (int i : s1 | std::views::transform([](const record& r) { return coalesce(0, r.v1, r.v2); }))

    {

        std::cout << i << " ";  // prints first 10, then 20 and at the end 0

    }

    std::cout << std::endl;

 

 

Another data feed, the 1st field of the record is in grams and the second in Kg

    std::vector<record> s2

    {

        {1000, {}},

        {{},    2},

        {{},   {}}

    };

 

To process this feed I need to define another class.

Coalesce requires that the input parameters have defined the Boolean operator (to check that the pointer is not null) and the indirection operators.

 

    template <typename Value, typename Proj>

    class test_project

    {

    public:

        test_project(Value v, Proj proj) : _v{ v }, _proj{ proj } {}

 

        bool operator! () const noexcept   {  return !_v;   }

       auto operator *()  {  return std::invoke(_proj, *_v);  }

 

    private:

        Value _v;

        Proj _proj;

    };

 

 

    int r9 = std::accumulate(s2.begin(), s2.end(), 0,

        [](int i, const record& r)

        { return i +

            coalesce(0, r.v1,

                test_project(r.v2,

                        [](int i) {return i * 1000; })); // with the help of test_project and a lambda

                                                                         // we convert the second field in grams

        }

    );

    std::cout << r9 << std::endl;  // prints 3000 = 1000 + 2 * 1000

 

 

 

 

I made an implementation of coalesce it can be found here: cpp_small_simple_stupid_stuff/Coalesce/Coalesce at main · roroberto/cpp_small_simple_stupid_stuff · GitHub

 

It is based on a set of concepts and 3 functions (2 are template specialization of the 1st one).

 

 

    /**

     * Concept that define a pointer of type PointerType that points to

     * ValueType. ValueType can be a function (an invocable).

     * It covers: raw pointers, std::unique_ptr, std:shared_ptr,

     * std::weak_ptr, std::optional, std::null_ptr, and any other

     * class that has the operators * and bool.

     */

    template<typename PointerType, typename ValueType>

    concept pointer_to =

    (    std::invocable<ValueType>

         &&

         (

            requires(PointerType pointer, ValueType value)  // Pointer must behave like a raw pointer to an object of class decltype(value())

            {

                {!pointer};

                {*pointer} -> std::convertible_to<decltype(value())>;

            }

            || requires(PointerType pointer, ValueType value) // Pointer must behave like a weak_ptr that points to an object of class decltype(value())

            {

                {*pointer.lock()} -> std::convertible_to<decltype(value())>;

            }

         )

    )

    ||

    (   !std::invocable<ValueType>

        &&

        (

            requires(PointerType pointer, ValueType)  // Pointer must behave like a raw pointer that points to a ValueType

            {

                {!pointer};

                {*pointer} -> std::convertible_to<ValueType>;

            }

            || requires(PointerType pointer, ValueType) // Pointer must behave like a weak_ptr that points to a ValueType

            {

                {*pointer.lock()} -> std::convertible_to<ValueType>;

            }

        )

    )

    || std::same_as<std::nullptr_t, PointerType>; // to cover the case of null_ptr

 

 

    /**

     * Concept that define the requirements of the coalesce parameters, they can

     * be pointers or functions that return pointers.

     */

    template<typename PointerType, typename ValueType>

    concept coalesce_param =

    requires(PointerType callable, ValueType)

    {

        {callable()} -> pointer_to<ValueType>;

    }

    || pointer_to<PointerType, ValueType>;

 

 

/**

    * It looks for a not null value in to_test_v. If it does

    * not find it, then coalesce returns default_value. It is similar to

    * a SQL coalesce function.

      *

    * \param default_value Value to return if to_test_0 and all to_test_v are null

    * \param ...to_test_v Next values to check.

    * \return  It looks for the first element of to_test_v not null.

    *           If all the values are null then coalesce returns default_value.

    */

    template<typename ReturnType, typename DefaultType, coalesce_param<ReturnType>... Args>

    requires std::convertible_to< DefaultType, std::remove_reference_t<ReturnType> >

    constexpr decltype(auto) coalesce(DefaultType&& default_value, Args&&... to_test_v);

 

    /**

     * Specialized version of coalesce: the return type is a reference of

     * DefaultType and DefaultType is not invocable.

     */

    template<typename DefaultType, coalesce_param<DefaultType>... Args>

    requires (!std::invocable<DefaultType>)

    constexpr decltype(auto) coalesce(DefaultType&& default_value, Args&&... to_test_v);

 

    /**

     * Specialized version of coalesce: DefaultType is invocable, the return type of

     * coalesce is the same return type of the function passed as default parameter.

     */

    template<typename DefaultType, coalesce_param<DefaultType>... Args>

    requires std::invocable<DefaultType>

    constexpr std::invoke_result_t<DefaultType> coalesce(DefaultType&& default_value, Args&&... to_test_v);

 

 

What do you think about it?

 

Best Regards

Roberto