3

I have a class that takes in its constructor a std::function<void(T)>. If I declare T to be a double, but pass the constructor a lambda that takes an int, the compiler let's me get away without a warning. (Update: MSVC issues a warning, clang does not). Is it possible to modify this code so that I get a compiler error (w/ clang)?

I was surprised this works:

#include <functional>
#include <iostream>

template <typename T>
class Thing {
public:
  using Handler = std::function<void(T)>;

  Thing(Handler handler)
  : handler_(handler)
  {}

  Handler handler_;
};

int main() {

  Thing<double> foo([](int bar) {  // Conversion to void(int) from void(double)
    std::cout << bar;
  });

  foo.handler_(4.2);

  return 0;
}

...and compiles without a warning:

$ clang --std=c++11 -lc++ -Wall -Werror test.cc
$ ./a.out
4

It seems like such a conversion could result in unwanted side effects. I can't imagine a case where this would ever be desired behavior.

moof2k
  • 1,678
  • 1
  • 17
  • 19
  • 1
    I could be wrong, but I think it works because a double can be implicitly cast to an int, not that the function object is being converted. – Millie Smith Mar 25 '16 at 04:50
  • Relevant question: http://stackoverflow.com/questions/12877546/how-do-i-avoid-implicit-casting-on-non-constructing-functions-c – Millie Smith Mar 25 '16 at 04:56
  • 2
    It's not converting a std::function to a std::function. It's wrapping a lambda in a std::function. And you're actually explicitly requesting it. – xaxxon Mar 25 '16 at 05:00
  • This does post a warning in Visual Studios 2015 (C4244). – user2913685 Mar 25 '16 at 05:09
  • Ah, thank you @xaxxon. I'll try and update the question. I'm still curious if its possible to prevent this and why this would ever be desired.. interesting MSVC gives a warning. – moof2k Mar 25 '16 at 05:15
  • If the function you wrap in a `std::function` can be called with the arguments of the `std::function`, then it's OK. The fear, uncertainty and doubt about it, is unfounded. If you don't want it, just don't ask for it. – Cheers and hth. - Alf Mar 25 '16 at 05:30
  • To modify the code to accept only the precise signature that you want, is non-trivial. As I recall there are some 40+ cases to check, in general. But you can always just use an ordinary function pointer, i.e. choose to not support capturing lambdas and other stateful functors. I can't see any point in doing that, though. I can't see any advantage. – Cheers and hth. - Alf Mar 25 '16 at 05:34
  • You could write your own "make_function" function that only takes functions with exactly the parameters you specify, but that would be kind of ugly. make_function(some_lambda); – xaxxon Mar 25 '16 at 06:33
  • When you think about std::function, remember, it holds anything that can be called with the specified types. Just like if you write void myfunc(float a); you can't stop it from being called like: myfunc(2), a std::function behaves the same way. – xaxxon Mar 25 '16 at 06:34

2 Answers2

3

You can use for the parameter a template class which allows conversion only to T:

template <typename T>
struct Strict {
    // This is a bit simplistic, but enough for fundamental types
    Strict(T t_) : t(t_) {}

    operator T() const { return t; }

    template<typename U>
    operator U() const = delete;

    T t;
};

template <typename T>
class Thing {
public:
    using Handler = std::function<void(Strict<T>)>;
    //...
};

DEMO

Note, however, that with this approach the widening conversions won't work as well:

// doesn't compile
Thing<int> foo{[](long long bar){});
Anton Savin
  • 40,838
  • 8
  • 54
  • 90
2

For non capturing lambdas, you can rely on the fact that they decay to pointers to functions.
Use this line:

using Handler = void(*)(T);

Instead of the current definition of Handler.
This way you'll receive an error, unless you change the argument type to the expected type.

In other words, this (does not) work as expected:

#include <functional>
#include <iostream>

  template <typename T>
  class Thing {
  public:
    using Handler = void(*)(T);

    Thing(Handler handler)
    : handler_(handler)
    {}

    Handler handler_;
};

int main() {
    // switch the type of bar to double and it will compile
    Thing<double> foo([](int bar) {
        std::cout << bar;
    });

    foo.handler_(4.2);
    return 0;
}

Be aware that this solution no longer works if you have a capture list for your lambdas.
Anyway, it solves the issue as explained in the question.

skypjack
  • 49,335
  • 19
  • 95
  • 187