0
\$\begingroup\$

Background

I am writing a library that takes some data from the user and works with it. I was experimenting with ways to allow users to provide the data by the following methods:

  • As a (global) variable (taken by a const &)
  • As a function pointer
  • As a lambda (taken by value)

I am aware that I can just wrap the variable or function in a lambda to achieve the same result, but I think I can learn something by doing it the "hard" way.

I am targeting C++20.

Code

#include <cassert>
#include <cstdint>
#include <functional>

template <class T, bool = std::is_invocable_v<T>>
struct GetDataType
{
    using type = typename std::invoke_result_t<T>;
};

template <class T>
struct GetDataType<T, false>
{
    using type = T;
};

template <class T, bool = std::is_invocable_v<T>>
struct GetSourceType
{
    using type = T;
};

template <class T>
struct GetSourceType<T, false>
{
    using type = typename std::add_lvalue_reference_t<std::add_const_t<T>>;
};

template <typename DataSource>
class DataGetter
{
public:
    using DataType = GetDataType<DataSource>::type;
    using DataSourceType = GetSourceType<DataSource>::type;

    DataGetter(const DataSource &f) : m_dataSource(f) {}

    [[nodiscard]] bool dataMatches(DataType refData) const
    {
        return refData == getData();
    }

private:
    DataSourceType m_dataSource;

    [[nodiscard]] DataType getData() const
    {
        if constexpr (std::invocable<DataSource>)
        {
            return std::invoke(m_dataSource);
        }
        else
        {
            return m_dataSource;
        }
    }
};

volatile uint32_t g_Data = 3;
uint32_t getData() { return g_Data + 1; }

int main(int argc, char **)
{
    DataGetter fromLocal(argc);
    DataGetter fromGlobal(g_Data);
    DataGetter fromLambda([&argc]()
                          { return argc + 1; });
    DataGetter fromFunctionPointer(&getData);

    assert(fromLocal.dataMatches(1));
    assert(fromGlobal.dataMatches(3));
    assert(fromLambda.dataMatches(2));
    assert(fromFunctionPointer.dataMatches(4));

    argc++;
    g_Data = g_Data + 1;

    assert(fromLocal.dataMatches(2));
    assert(fromGlobal.dataMatches(4));
    assert(fromLambda.dataMatches(3));
    assert(fromFunctionPointer.dataMatches(5));

    return 0;
}

Compiler Explorer

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Your initial traits can be simplified using std::conditional:

template <typename T>
using GetDataType = std::conditional_t<std::is_invocable_v<T>,
                                       std::invoke_result<T>,
                                       std::type_identity<T>>::type;

template <typename T>
using GetSourceType = std::conditional_t<std::is_invocable_v<T>,
                                       std::type_identity_t<T>,
                                       std::add_lvalue_reference_t<std::add_const_t<T>>>;

(From usage I would use const T& for non invokable for GetDataType)

Then

template <typename DataSource>
class DataGetter
{
public:
    using DataType = GetDataType<DataSource>;
    using DataSourceType = GetSourceType<DataSource>;

    DataGetter(const DataSource &f) : m_dataSource(f) {}

    [[nodiscard]] bool dataMatches(DataType refData) const
    {
        return refData == getData();
    }

private:
    DataSourceType m_dataSource;

    [[nodiscard]] DataType getData() const
    {
        if constexpr (std::invocable<DataSource>)
        {
            return std::invoke(m_dataSource);
        }
        else
        {
            return m_dataSource;
        }
    }
};

Demo

\$\endgroup\$
3
  • \$\begingroup\$ Thank you! I did initially try use std::conditional, but was getting errors. I see you used std::conditional_t and ::type at the end for GetDataType, but GetSourceType does not have ::type at the end. Could you help me understand how this works? \$\endgroup\$
    – Armandas
    Feb 2, 2022 at 23:42
  • \$\begingroup\$ I think I got it, if I rewrite it as below, then it works too: template <typename T> using GetSourceType = std::conditional_t<std::is_invocable_v<T>, std::type_identity<T>, std::add_lvalue_reference<std::add_const_t<T>>>::type; \$\endgroup\$
    – Armandas
    Feb 2, 2022 at 23:50
  • 1
    \$\begingroup\$ std::invoke_result<T> is valid for any T, but std::invoke_result<T>::type is only valid if T is invocable, so we have to delay its instantiation (so usage of std::type_identity<T> on the other side). and then the possibly strange std::conditional<..>::type::type. \$\endgroup\$
    – Jarod42
    Feb 3, 2022 at 9:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.