moof2k
moof2k

Reputation: 1897

Prevent wrapping lambda in std::function with different types

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.

Upvotes: 3

Views: 323

Answers (2)

skypjack
skypjack

Reputation: 50568

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.

Upvotes: 2

Anton Savin
Anton Savin

Reputation: 41331

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){});

Upvotes: 3

Related Questions