C-style wrapper to C++ functor

112 Views Asked by At

This is further developing on what I was trying at this recent question of mine.

I want to create a bool(double, double) C-style function pointer to a functor with compatible signature. (The reason for the further attempt is that the earlier approach seemed to have restrictions on what can be constexpr-ed.)

How can I do in C++ the equivalent of the following working Python code:

#! /usr/bin/env python3

class FixPrecCompare:
    '''Compares two floats to the given precision'''
    def __init__(self, numDigits: int) -> None:
        self.factor = 10 ** numDigits
    def __call__(self, a: float, b: float) -> bool:
        return int(a * self.factor) == int(b * self.factor)
    
class AreClose:
    '''Same as Python's inbuilt math.isclose [which should be called areclose :-)]'''
    def __init__(self, absTol = 0, relTol = 1e-09) -> None:
        self.absTol = absTol; self.relTol = relTol
    def __call__(self, a: float, b: float) -> bool:
        diff = abs(a - b)
        return diff <= self.absTol or \
            diff <= abs(self.relTol * a) or \
            diff <= abs(self.relTol * b)

# Now for signature compatibility (which is not a matter in Python)
# I make a function which internally uses the functor
def makeApproxCompare(FunctorType: type, *args):
    functor = FunctorType(*args)
    def fn(a: float, b: float) -> bool:
        return functor(a, b)
    return fn

approxCompare2 = makeApproxCompare(FixPrecCompare, 2)
approxCompare3 = makeApproxCompare(FixPrecCompare, 3)
areClose = makeApproxCompare(AreClose, 1e-3)

a, b = 4.561, 4.569
print(f"Compare {a:.3f}, {b:.3f} to 2 decimals: {approxCompare2(a, b)}")
print(f"Compare {a:.3f}, {b:.3f} to 3 decimals: {approxCompare3(a, b)}")

a, b = 2.7099, 2.7101
print(f"Compare {a} to {b} to 3 decimals: {approxCompare3(a, b)}")
print(f"Compare {a} to {b} using areClose: {areClose(a, b)}")

My attempt:

typedef bool(*SuccessTester)(double, double);

#include <cmath>
#include <iostream>
using namespace std;

struct FixPrecCompare
{
    FixPrecCompare(int numDigits): factor(pow(10, numDigits)) {}
    bool operator()(double a, double b) { return int(a * factor) == int(b * factor); }
private:
    double factor;
};

struct AreClose
{
    AreClose(double absTol, double relTol): abs_tol{absTol}, rel_tol{relTol} {}
    bool operator()(double a, double b)
    {
        double diff = fabs(a - b);
        return diff <= abs_tol ||
            diff <= fabs(rel_tol * a) ||
            diff <= fabs(rel_tol * b);
    }
private:
    double abs_tol, rel_tol;
};

template<typename Functor>
struct Helper
{
    static Functor fr;
    static bool func(double a, double b) { return fr(a, b); }
};

template<typename Functor, typename ... ArgTypes>
SuccessTester makeSuccessTester(ArgTypes ... args)
{
    Helper<Functor> h;
    h.fr = Functor{args ...};
    return &h.func;
}

int main()
{
    SuccessTester fp;
    cout << boolalpha;

    double a, b;
    
    a = 4.561; b = 4.569;
    fp = makeSuccessTester<FixPrecCompare>(2);
    cout << "Compare " << a << ", " << b << " to 2 decimals: " << fp(a, b) << endl;
    fp = makeSuccessTester<FixPrecCompare>(3);
    cout << "Compare " << a << ", " << b << " to 3 decimals: " << fp(a, b) << endl;
    
    a = 2.7099; b = 2.7101;
    cout << "Compare " << a << ", " << b << " to 3 decimals: " << fp(a, b) << endl;
    fp = makeSuccessTester<AreClose>(1e-3, 1e-10);
    cout << "Compare " << a << ", " << b << " using areClose: " << fp(a, b) << endl;
}

But on GCC this gives:

/usr/bin/ld: /tmp/ccEkJxbk.o: warning: relocation against `_ZN6HelperI14FixPrecCompareE2frE' in read-only section `.text._ZN6HelperI14FixPrecCompareE4funcEdd[_ZN6HelperI14FixPrecCompareE4funcEdd]'
/usr/bin/ld: /tmp/ccEkJxbk.o: in function `bool (*makeSuccessTester<FixPrecCompare, int>(int))(double, double)':
<src>:(.text._Z17makeSuccessTesterI14FixPrecCompareJiEEPFbddEDpT0_[_Z17makeSuccessTesterI14FixPrecCompareJiEEPFbddEDpT0_]+0x38): undefined reference to `Helper<FixPrecCompare>::fr'
/usr/bin/ld: /tmp/ccEkJxbk.o: in function `bool (*makeSuccessTester<AreClose, double, double>(double, double))(double, double)':
<src>:(.text._Z17makeSuccessTesterI8AreCloseJddEEPFbddEDpT0_[_Z17makeSuccessTesterI8AreCloseJddEEPFbddEDpT0_]+0x4e): undefined reference to `Helper<AreClose>::fr'
/usr/bin/ld: <src>:(.text._Z17makeSuccessTesterI8AreCloseJddEEPFbddEDpT0_[_Z17makeSuccessTesterI8AreCloseJddEEPFbddEDpT0_]+0x55): undefined reference to `Helper<AreClose>::fr'
/usr/bin/ld: /tmp/ccEkJxbk.o: in function `Helper<FixPrecCompare>::func(double, double)':
<src>:(.text._ZN6HelperI14FixPrecCompareE4funcEdd[_ZN6HelperI14FixPrecCompareE4funcEdd]+0x2b): undefined reference to `Helper<FixPrecCompare>::fr'
/usr/bin/ld: /tmp/ccEkJxbk.o: in function `Helper<AreClose>::func(double, double)':
<src>:(.text._ZN6HelperI8AreCloseE4funcEdd[_ZN6HelperI8AreCloseE4funcEdd]+0x2b): undefined reference to `Helper<AreClose>::fr'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status

And Clang is somewhat better:

<src>:40:7: warning: instantiation of variable 'Helper<FixPrecCompare>::fr' required here, but no definition is available [-Wundefined-var-template]
    h.fr = Functor{args ...};
      ^
<src>:52:10: note: in instantiation of function template specialization 'makeSuccessTester<FixPrecCompare, int>' requested here
    fp = makeSuccessTester<FixPrecCompare>(2);
        ^
<src>:32:20: note: forward declaration of template entity is here
    static Functor fr;
                  ^
<src>:40:7: note: add an explicit instantiation declaration to suppress this warning if 'Helper<FixPrecCompare>::fr' is explicitly instantiated in another translation unit
    h.fr = Functor{args ...};
      ^
<src>:40:7: warning: instantiation of variable 'Helper<AreClose>::fr' required here, but no definition is available [-Wundefined-var-template]
    h.fr = Functor{args ...};
      ^
<src>:59:10: note: in instantiation of function template specialization 'makeSuccessTester<AreClose, double, double>' requested here
    fp = makeSuccessTester<AreClose>(1e-3, 1e-10);
        ^
<src>:32:20: note: forward declaration of template entity is here
    static Functor fr;
                  ^
<src>:40:7: note: add an explicit instantiation declaration to suppress this warning if 'Helper<AreClose>::fr' is explicitly instantiated in another translation unit
    h.fr = Functor{args ...};
      ^
2 warnings generated.
/usr/bin/ld: /tmp/<src>-ce7af1.o: in function `bool (*makeSuccessTester<FixPrecCompare, int>(int))(double, double)':
<src>:(.text._Z17makeSuccessTesterI14FixPrecCompareJiEEPFbddEDpT0_[_Z17makeSuccessTesterI14FixPrecCompareJiEEPFbddEDpT0_]+0x1a): undefined reference to `Helper<FixPrecCompare>::fr'
/usr/bin/ld: /tmp/<src>-ce7af1.o: in function `bool (*makeSuccessTester<AreClose, double, double>(double, double))(double, double)':
<src>:(.text._Z17makeSuccessTesterI8AreCloseJddEEPFbddEDpT0_[_Z17makeSuccessTesterI8AreCloseJddEEPFbddEDpT0_]+0x28): undefined reference to `Helper<AreClose>::fr'
/usr/bin/ld: /tmp/<src>-ce7af1.o: in function `Helper<FixPrecCompare>::func(double, double)':
<src>:(.text._ZN6HelperI14FixPrecCompareE4funcEdd[_ZN6HelperI14FixPrecCompareE4funcEdd]+0x1f): undefined reference to `Helper<FixPrecCompare>::fr'
/usr/bin/ld: /tmp/<src>-ce7af1.o: in function `Helper<AreClose>::func(double, double)':
<src>:(.text._ZN6HelperI8AreCloseE4funcEdd[_ZN6HelperI8AreCloseE4funcEdd]+0x1f): undefined reference to `Helper<AreClose>::fr'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

But I'm not sure how to fix the code to make it work as desired. Please help! Thanks!

2

There are 2 best solutions below

4
aparpara On

The principal problem with your approach is that you are trying to assign several different values to the same static member at once. Helper<FixPrecCompare>::fr is a unique variable, because you declared it static, and it can't hold both FixPrecCompare(2) and FixPrecCompare(3) at the same time. So even if you make this compile, it will work as a global variable containing the last value you assigned to it, which is not a good thing to have anyways. I see 2 solutions here: either make everything a template parameter or use type erasure provided by std::function.

  1. Works starting with C++20, as we need double template parameters here:
#include <cmath>
#include <iostream>
using namespace std;

typedef bool(*SuccessTester)(double, double);

template<int numDigits>
struct FixPrecCompare
{
    static bool compare(double a, double b) { return int(a * factor) == int(b * factor); }
private:
    static constexpr double factor = pow(10, numDigits);
};

template<double abs_tol, double rel_tol>
struct AreClose
{
    static bool compare(double a, double b)
    {
        double diff = fabs(a - b);
        return diff <= abs_tol ||
            diff <= fabs(rel_tol * a) ||
            diff <= fabs(rel_tol * b);
    }
};

template<typename Functor>
SuccessTester makeSuccessTester()
{
    return &Functor::compare;
}

int main()
{
    SuccessTester fp;
    cout << boolalpha;

    double a, b;
    
    a = 4.561; b = 4.569;
    fp = makeSuccessTester<FixPrecCompare<2>>();
    cout << "Compare " << a << ", " << b << " to 2 decimals: " << fp(a, b) << endl;
    fp = makeSuccessTester<FixPrecCompare<3>>();
    cout << "Compare " << a << ", " << b << " to 3 decimals: " << fp(a, b) << endl;
    
    a = 2.7099; b = 2.7101;
    cout << "Compare " << a << ", " << b << " to 3 decimals: " << fp(a, b) << endl;
    fp = makeSuccessTester<AreClose<1e-3, 1e-10>>();
    cout << "Compare " << a << ", " << b << " using areClose: " << fp(a, b) << endl;
}
#include <cmath>
#include <iostream>
#include <functional>
using namespace std;

using SuccessTester = std::function<bool(double, double)>;

struct FixPrecCompare
{
    FixPrecCompare(int numDigits): factor(pow(10, numDigits)) {}
    bool operator()(double a, double b) { return int(a * factor) == int(b * factor); }
private:
    double factor;
};

struct AreClose
{
    AreClose(double absTol, double relTol): abs_tol{absTol}, rel_tol{relTol} {}
    bool operator()(double a, double b)
    {
        double diff = fabs(a - b);
        return diff <= abs_tol ||
            diff <= fabs(rel_tol * a) ||
            diff <= fabs(rel_tol * b);
    }
private:
    double abs_tol, rel_tol;
};

template<typename Functor, typename ... ArgTypes>
SuccessTester makeSuccessTester(ArgTypes ... args)
{
    return Functor{args ...};
}

int main()
{
    SuccessTester fp;
    cout << boolalpha;

    double a, b;
    
    a = 4.561; b = 4.569;
    fp = makeSuccessTester<FixPrecCompare>(2);
    cout << "Compare " << a << ", " << b << " to 2 decimals: " << fp(a, b) << endl;
    fp = makeSuccessTester<FixPrecCompare>(3);
    cout << "Compare " << a << ", " << b << " to 3 decimals: " << fp(a, b) << endl;
    
    a = 2.7099; b = 2.7101;
    cout << "Compare " << a << ", " << b << " to 3 decimals: " << fp(a, b) << endl;
    fp = makeSuccessTester<AreClose>(1e-3, 1e-10);
    cout << "Compare " << a << ", " << b << " using areClose: " << fp(a, b) << endl;
}
0
alagner On

I would consider making it a macro:

#define MAKE_SUCCESS_TESTER(F, ...)\
+[] (double x, double y) { \
auto fr = F{__VA_ARGS__}; \
return fr(x,y); \
}

It operates on the principle of creating a local, non-capturing lambda. These are implicitly convertible to function pointers. The unary+ is not mandatory, but acts as a shorthand for static_cast<SuccessTester>(/*lambda goes here*/); enabling usage of type deduction when initializing the pointer and allowing somewhat better error diagnostic if capturing lambda is used accidentally: Details here.

auto fp1 = MAKE_SUCCESS_TESTER_NO_PLUS_CAPTURING(FixPrecCompare, 2);
static_assert(std::is_same_v<decltype(fp1), SuccessTester>);

I has the following drawbacks:

  1. Type safety is gone. It's a macro; we could possibly fiddle with some static_asserts or similar inside the lambda, but fundamentally we're on our own here.
  2. Passing immediate arguments might becomed complicated. It's fine for doubles, but trying to pass some initializer list there might result in an ugly compilation error. Again, typical for macros, but it's good to be aware of it.
  3. It creates the inner fr object each time it's called. auto fr = F{__VA_ARGS__}; can be made static, but then it would involve thread safe initialization, possibly a mutex being locked. Moreover, using this macro with the same parameters in a different translation unit will create a new lambda with a new static comparator inside. This is not necessarily bad, but it is something to be aware of.

If this still appeals to you, here is an example: demo.