2

Using g++ (Ubuntu 4.8.5-1ubuntu1) 4.8.5 and compiling with g++ -std=c++11 -Wall -Wextra -Wconversion

The following does not compile which is as expected:

template <typename T>
struct Foo {
    Foo(T t) {}
};

struct Bar {
    Bar(Foo<float> foo) : foo(foo) {} //Trying to convert Foo<float> to Foo<double>
    Foo<double> foo;
};

The following compiles with a warning from -Wconversion, as expected:

void foo(float t){}
int main() {
    foo(3.141592653589794626);
    return 0;   
}

However, the following compiles with no warnings:

#include <functional>

void foo(double t){}

struct Bar {
    Bar(std::function<void(float)> foo) : foo(foo) {} //Convert std::function<void(float)> to std::function<void(double)>
    std::function<void(double)> foo;
};

int main(){
    Bar bar(foo); //Convert std::function<void(double)> to std::function<void(float)>
    bar.foo(3.141592653589794626); //Rounded  to: 3.141592741012573
    foo(3.141592653589794626);     //Not rounded: 3.141592653589794
    return 0;
}

Clearly this is some automatic conversion float<->double but why is it allowed in the third example and not the first? Why does -Wconversion not catch this?

(Invisible loss of precision is a problem in a number of areas, for example when working with latitude/longitude).

lenguador
  • 476
  • 5
  • 9

2 Answers2

3

As Elwin Arens noted, the problem is with the type erasure going on in the inner workings of std::function. One might suppose that a quick fix would be to change the type in the constructor argument to double, but that doesn't prevent the user from passing in a function that takes a float. For example,

void foo(float t) {
    std::cout << std::setprecision(15) << std::fixed << t << std::endl;
}

struct Bar {
    Bar(std::function<void(double)> foo) : foo(foo) {}
    std::function<void(double)> foo;
};

int main() {
    Bar bar(foo);
    bar.foo(3.141592653589794626); //Rounded  to: 3.141592741012573
        foo(3.141592653589794626); //Not rounded: 3.141592653589794
}

compiles file, but gives the undesired result. One fix it to use a template constructor and some TMP.

void foo(double t) {
    std::cout << std::setprecision(15) << std::fixed << t << std::endl;
}

struct Bar {
    using target_type = double;
    using func_type = void(*)(target_type);

    template <typename T, typename U = typename std::enable_if<std::is_same<T,func_type>::value,void>::type>
    Bar(T foo) : foo(foo) {}

    std::function<void(target_type)> foo;
};

int main() {
    Bar bar(foo);
    bar.foo(3.141592653589794626); //Rounded  to: 3.141592741012573
        foo(3.141592653589794626); //Not rounded: 3.141592653589794
}

Now it fails to compile if you pass in a function that doesn't match the signature of Bar::foo. The complication is that you have to make sure that func_type also matches the signature of Bar::foo if ever it changes.

Tim
  • 1,517
  • 1
  • 9
  • 15
  • Thank you Tim. After some research from Elwin Arens' answer I believe in this case the best definition of Bar is: `template struct Bar { Bar(const F &foo) : foo(foo) {} F &foo; }; ` . I should not have been using std::function to begin with! – lenguador Jun 11 '16 at 07:57
  • @lenguador Be careful with this solution. If `F` is a lambda with a lifetime shorter than the `Bar` being instantiated, you will go into bad places. I agree that this solution makes for better type safety, but it also makes for convoluted declarations: `int meow(float); auto b = Bar(meow);` – Tim Jun 11 '16 at 08:18
  • Is there some way to ensure that a function is provided at compile time? In my use case the function is known at compile time but I wouldn't want someone to erroneously pass a lambda with an incorrect lifetime. – lenguador Jun 11 '16 at 08:31
  • I spoke too soon. I tested your solution, and it *doesn't* compile when using a lambda because `Bar::foo` isn't `const`. Evidently function pointers are allowed to discard CV qualifiers. If you want to be super careful, check out [std::is_function](http://en.cppreference.com/w/cpp/types/is_function). – Tim Jun 11 '16 at 08:54
2

This has to do with the purpose of std::function for being for runtime polymorphism and hence it uses type erasure, which has been discussed here here and here

Community
  • 1
  • 1
Elwin Arens
  • 1,542
  • 10
  • 21