How to Implement Mixed Storage of Function Pointers, Function Objects, and Lambda Expressions in C++

In previous articles, we discussed function pointers, function objects, and lambda expressions. We learned that stateless lambda expressions can be implicitly converted to function pointers of the same type, and stateless function objects can also be converted after manually adding conversion operations. However, stateful lambda expressions and function objects cannot be converted to function pointers because function pointers cannot carry state information. Therefore, when using them as callback function parameters, we can only use template type parameters for unified operations. But are there other methods? Moreover, if we need to store them uniformly, template type parameters won’t work either. So how should we proceed? Without further ado, let’s define a few function pointers, function objects, and lambda expressions. The code is as follows:

#include <iostream>

// Stateless function object
class SumType
{
public:
    int operator()(int x, int y)
    {
        std::cout << "Calling stateless function object result: ";
        int sum = x + y;
        return sum;
    }
};

// Stateful function object
class StSumType
{
public:
    StSumType(int n) { num = n; }
    int operator()(int x, int y)
    {
        std::cout << "Calling stateful function object result: ";
        int sum = x + y + num;
        return sum;
    }
private:
    int num;
};

int sumfunc(int x, int y)
{
    std::cout << "Calling function pointer result: ";
    return x + y;
}

int main()
{
    int a = 5, b = 3, c = 7;
    // Stateless lambda
    auto lam = [](int x, int y)
               {
                   std::cout << "Calling stateless lambda result: ";
                   return x + y;
               };
    // Stateful lambda
    auto lams = [a](int x, int y)
               {
                   std::cout << "Calling stateful lambda result: ";
                   return x + y + a;
               };
    // Function pointer
    int(*pf)(int x, int y) = sumfunc;

    // Call function pointer
    std::cout << pf(b, c) << std::endl;
    // Call stateless function object
    SumType so;
    std::cout << so(b, c) << std::endl;
    // Call stateful function object
    StSumType stso(a);
    std::cout << stso(b, c) << std::endl;
    // Call stateless lambda
    std::cout << lam(b, c) << std::endl;
    // Call stateful lambda
    std::cout << lams(b, c) << std::endl;
    return 0;
}

The compilation and execution results are as follows:

How to Implement Mixed Storage of Function Pointers, Function Objects, and Lambda Expressions in C++

Above are the individual calls. Now let’s see how to use template parameters for unified calls. The modified code is as follows:

#include <iostream>

// Stateless function object
class SumType
{
public:
    int operator()(int x, int y)
    {
        std::cout << "Calling stateless function object result: ";
        int sum = x + y;
        return sum;
    }
};

// Stateful function object
class StSumType
{
public:
    StSumType(int n) { num = n; }
    int operator()(int x, int y)
    {
        std::cout << "Calling stateful function object result: ";
        int sum = x + y + num;
        return sum;
    }
private:
    int num;
};

int sumfunc(int x, int y)
{
    std::cout << "Calling function pointer result: ";
    return x + y;
}

template<typename T>
int invokfunc(int x, int y, T func)
{
    return func(x, y);
}

int main()
{
    int a = 5, b = 3, c = 7;
    // Stateless lambda
    auto lam = [](int x, int y)
               {
                   std::cout << "Calling stateless lambda result: ";
                   return x + y;
               };
    // Stateful lambda
    auto lams = [a](int x, int y)
               {
                   std::cout << "Calling stateful lambda result: ";
                   return x + y + a;
               };
    // Function pointer
    int(*pf)(int x, int y) = sumfunc;

    // Call function pointer
    std::cout << invokfunc(b, c, pf) << std::endl;
    // Call stateless function object
    SumType so;
    std::cout << invokfunc(b, c, so) << std::endl;
    // Call stateful function object
    StSumType stso(a);
    std::cout << invokfunc(b, c, stso) << std::endl;
    // Call stateless lambda
    std::cout << invokfunc(b, c, lam) << std::endl;
    // Call stateful lambda
    std::cout << invokfunc(b, c, lams) << std::endl;
    return 0;
}

The execution results are the same as before:

How to Implement Mixed Storage of Function Pointers, Function Objects, and Lambda Expressions in C++

This method can achieve unification in calls, but can it do so in storage? Obviously not, as template parameters must confirm types at compile time, and once a specific type is determined, others cannot be stored. To address this, C++ provides the std::function<> template type, which can store, copy, move, and call any callable entity, whether it is a function pointer, function object, or lambda. We modify the code again using std::function<> as follows:

#include <iostream>
#include <functional>

// Stateless function object
class SumType
{
public:
    int operator()(int x, int y)
    {
        std::cout << "Calling stateless function object result: ";
        int sum = x + y;
        return sum;
    }
};

// Stateful function object
class StSumType
{
public:
    StSumType(int n) { num = n; }
    int operator()(int x, int y)
    {
        std::cout << "Calling stateful function object result: ";
        int sum = x + y + num;
        return sum;
    }
private:
    int num;
};

int sumfunc(int x, int y)
{
    std::cout << "Calling function pointer result: ";
    return x + y;
}

template<typename T>
int invokfunc(int x, int y, T func)
{
    return func(x, y);
}

int main()
{
    int a = 5, b = 3, c = 7;
    // Stateless lambda
    auto lam = [](int x, int y)
               {
                   std::cout << "Calling stateless lambda result: ";
                   return x + y;
               };
    // Stateful lambda
    auto lams = [a](int x, int y)
               {
                   std::cout << "Calling stateful lambda result: ";
                   return x + y + a;
               };
    // Function pointer
    int(*pf)(int x, int y) = sumfunc;

    // Define std::function<> type object
    std::function<int(int, int)> sum;
    // Call function pointer
    sum = pf;
    std::cout << sum(b, c) << std::endl;
    // Call stateless function object
    SumType so;
    sum = so;
    std::cout << sum(b, c) << std::endl;
    // Call stateful function object
    StSumType stso(a);
    sum = stso;
    std::cout << sum(b, c) << std::endl;
    // Call stateless lambda
    sum = lam;
    std::cout << sum(b, c) << std::endl;
    // Call stateful lambda
    sum = lams;
    std::cout << sum(b, c) << std::endl;
    return 0;
}

The execution results are the same as before. From the code, we can see that std::function<> itself is also a function object. After defining it, we assign function pointers, function objects, and lambdas to it and then call them. The only restriction for assignment is that the return type and parameters must match its definition, which are specified in the angle brackets of the std::function<> type template. Since function pointers, function objects, and lambdas can all be encapsulated by std::function<>, we can achieve unified storage. The code is as follows:

#include <iostream>
#include <functional>
#include <vector>

// Stateless function object
class SumType
{
public:
    int operator()(int x, int y)
    {
        std::cout << "Calling stateless function object result: ";
        int sum = x + y;
        return sum;
    }
};

// Stateful function object
class StSumType
{
public:
    StSumType(int n) { num = n; }
    int operator()(int x, int y)
    {
        std::cout << "Calling stateful function object result: ";
        int sum = x + y + num;
        return sum;
    }
private:
    int num;
};

int sumfunc(int x, int y)
{
    std::cout << "Calling function pointer result: ";
    return x + y;
}

template<typename T>
int invokfunc(int x, int y, T func)
{
    return func(x, y);
}

int main()
{
    int a = 5, b = 3, c = 7;
    // Stateless lambda
    auto lam = [](int x, int y)
               {
                   std::cout << "Calling stateless lambda result: ";
                   return x + y;
               };
    // Stateful lambda
    auto lams = [a](int x, int y)
               {
                   std::cout << "Calling stateful lambda result: ";
                   return x + y + a;
               };
    // Function pointer
    int(*pf)(int x, int y) = sumfunc;

    // Define a container to store function pointers, function objects, and lambdas
    std::vector<std::function<int(int, int)> > vec;
    vec.push_back(pf);
    SumType so;
    vec.push_back(so);
    StSumType stso(a);
    vec.push_back(stso);
    vec.push_back(lam);
    vec.push_back(lams);
    // Call each function
    for(int i = 0; i < vec.size(); ++i)
    {
        std::cout << vec[i](b, c) << std::endl;
    }
    return 0;
}

The code implements mixed storage of function pointers, function objects, and lambdas, and calls them in a loop. The compilation and execution results are the same as before.

Thus, we have achieved our goal. Are there other methods? The answer is yes, although they may not be as convenient. C++17 introduced std::variant<>, and we can use it to see how it works. The code is as follows:

#include <iostream>
#include <variant>
#include <vector>

// Stateless function object
class SumType
{
public:
    int operator()(int x, int y)
    {
        std::cout << "Calling stateless function object result: ";
        int sum = x + y;
        return sum;
    }
};

// Stateful function object
class StSumType
{
public:
    StSumType(int n) { num = n; }
    int operator()(int x, int y)
    {
        std::cout << "Calling stateful function object result: ";
        int sum = x + y + num;
        return sum;
    }
private:
    int num;
};

int sumfunc(int x, int y)
{
    std::cout << "Calling function pointer result: ";
    return x + y;
}

template<typename T>
int invokfunc(int x, int y, T func)
{
    return func(x, y);
}

int main()
{
    int a = 5, b = 3, c = 7;
    // Stateless lambda
    auto lam = [](int x, int y)
               {
                   std::cout << "Calling stateless lambda result: ";
                   return x + y;
               };
    // Stateful lambda
    auto lams = [a](int x, int y)
               {
                   std::cout << "Calling stateful lambda result: ";
                   return x + y + a;
               };
    // Function pointer
    int(*pf)(int x, int y) = sumfunc;

    // Define a container to store function pointers, function objects, and lambdas
    std::vector<std::variant<decltype(pf), SumType, StSumType, decltype(lam), decltype(lams)> > vec;
    vec.push_back(pf);
    SumType so;
    vec.push_back(so);
    StSumType stso(a);
    vec.push_back(stso);
    vec.push_back(lam);
    vec.push_back(lams);

    int i = 0;
    std::cout << std::get<0>(vec[i++])(b, c) << std::endl;
    std::cout << std::get<1>(vec[i++])(b, c) << std::endl;
    std::cout << std::get<2>(vec[i++])(b, c) << std::endl;
    std::cout << std::get<3>(vec[i++])(b, c) << std::endl;
    std::cout << std::get<4>(vec[i++])(b, c) << std::endl;
    return 0;
}

std::variant<> is a type-safe union introduced in C++17 that can store multiple different types of values. However, as shown in the code above, it requires you to list all the types you need to store in the angle brackets<>, and when retrieving stored values, you also need to match the type. Since the type is determined at compile time, dynamic variables cannot be used. Therefore, while it can achieve the same functionality, it is not as convenient as std::function<>. Besides this, std::any was also introduced in C++17, and let’s look at the code:

#include <iostream>
#include <any>
#include <vector>

// Stateless function object
class SumType
{
public:
    int operator()(int x, int y)
    {
        std::cout << "Calling stateless function object result: ";
        int sum = x + y;
        return sum;
    }
};

// Stateful function object
class StSumType
{
public:
    StSumType(int n) { num = n; }
    int operator()(int x, int y)
    {
        std::cout << "Calling stateful function object result: ";
        int sum = x + y + num;
        return sum;
    }
private:
    int num;
};

int sumfunc(int x, int y)
{
    std::cout << "Calling function pointer result: ";
    return x + y;
}

template<typename T>
int invokfunc(int x, int y, T func)
{
    return func(x, y);
}

int main()
{
    int a = 5, b = 3, c = 7;
    // Stateless lambda
    auto lam = [](int x, int y)
               {
                   std::cout << "Calling stateless lambda result: ";
                   return x + y;
               };
    // Stateful lambda
    auto lams = [a](int x, int y)
               {
                   std::cout << "Calling stateful lambda result: ";
                   return x + y + a;
               };
    // Function pointer
    int(*pf)(int x, int y) = sumfunc;

    // Define a container to store function pointers, function objects, and lambdas
    std::vector<std::any> vec;
    vec.push_back(pf);
    SumType so;
    vec.push_back(so);
    StSumType stso(a);
    vec.push_back(stso);
    vec.push_back(lam);
    vec.push_back(lams);

    int i = 0;
    std::cout << std::any_cast<decltype(pf)>(vec[i++])(b, c) << std::endl;
    std::cout << std::any_cast<SumType>(vec[i++])(b, c) << std::endl;
    std::cout << std::any_cast<StSumType>(vec[i++])(b, c) << std::endl;
    std::cout << std::any_cast<decltype(lam)>(vec[i++])(b, c) << std::endl;
    std::cout << std::any_cast<decltype(lams)>(vec[i++])(b, c) <>< std::endl;
    return 0;
}

As shown in the code, compared to std::variant<>, std::any is much simpler when storing data, as it does not require specifying types and can store any data. However, when retrieving data, it becomes cumbersome, as you need to specify the type, and if the type is incorrect, it will throw an exception.

Overall, when storing function pointers, function objects, and lambdas, if they are similar types of function entities, using std::function<> is the most convenient. However, if the types of function entities to be stored are different, then std::variant<> and std::any come into play. As for which one to use, if you can determine the number of types to be stored in advance, use std::variant; if uncertain, use std::any. Finally, let’s give an example of storing different types of function entities, as shown in the code below:

#include <iostream>
#include <any>
#include <variant>
#include <vector>

// Stateful function object
class StSumType
{
public:
    StSumType(int n) { num = n; }
    int operator()(int x, int y)
    {
        std::cout << "Calling stateful function object result: ";
        int sum = x + y + num;
        return sum;
    }
private:
    int num;
};

float sqfunc(float x)
{
    std::cout << "Calling function pointer result: ";
    return x * x;
}

int main()
{
    int a = 5, b = 3, c = 7;
    float f = 1.1;
    // Stateful lambda
    auto lams = [a](int x)
               {
                   std::cout << "Calling stateful lambda result: ";
                   return x > a;
               };
    // Function pointer
    float(*pf)(float x) = sqfunc;

    // Using std::variant<>
    // Define a container to store function pointers, function objects, and lambdas
    std::vector<std::variant<decltype(pf), StSumType, decltype(lams)> > vec;
    vec.push_back(pf);
    StSumType stso(a);
    vec.push_back(stso);
    vec.push_back(lams);

    int i = 0;
    std::cout << std::get<0>(vec[i++])(f) << std::endl;
    std::cout << std::get<1>(vec[i++])(b, c) << std::endl;
    std::cout << std::get<2>(vec[i++])(b) << std::endl;

    // Using std::any
    // Define a container to store function pointers, function objects, and lambdas
    std::vector<std::any> vec2;
    vec2.push_back(pf);
    vec2.push_back(stso);
    vec2.push_back(lams);

    i = 0;
    std::cout << std::any_cast<decltype(pf)>(vec2[i++])(f) << std::endl;
    std::cout << std::any_cast<StSumType>(vec2[i++])(b, c) << std::endl;
    std::cout << std::any_cast<decltype(lams)>(vec2[i++])(b) << std::endl;
    return 0;
}

The compilation and execution results are as follows:

How to Implement Mixed Storage of Function Pointers, Function Objects, and Lambda Expressions in C++

In addition to these methods, there are other ways to achieve mixed storage, such as custom template classes, traditional C unions, and using third-party libraries like Boost, each with its advantages and disadvantages. Everyone can try them out.

Leave a Comment