Sergio Losilla
Sergio Losilla

Reputation: 850

Wrapping lambda as std::function produces wrong results (was The dangers of template argument deduction)

Returning a certain lambda wrapped as an std::function produces wrong results:

#include <functional>
#include <iostream>
#include <tuple>

template <typename T>
std::function<const T&()> constant(const T& c) {
  return [c]() noexcept -> const T&{ return c; };
}

template <typename T>
std::function<std::tuple<T>()> zip(const std::function<T()>& f) {
  return [f]() { return std::tuple{f()}; };
}

int main() {
    const auto good = [f = constant(1.0)]() { return std::tuple{f()}; };
    const auto bad = zip(constant(1.0));
    std::cout << "good: " << std::get<0>(good()) << std::endl;
    std::cout << "bad:  " << std::get<0>(bad()) << std::endl;
}

Output:

$ ./a.out
good: 1
bad:  6.95282e-310

It looks like undefined behaviour. What is going on?

Upvotes: 2

Views: 109

Answers (2)

Sergio Losilla
Sergio Losilla

Reputation: 850

The reason is the following: the tuple created inside zip is automatically deduced to be a std::tuple<double>, but zip returns a std::function<std::tuple<const double&>()>. The returned tuple contains a reference to a temporary, which is undefined behaviour.

The fix is simply explicitly adding the type of the tuple element:

template <typename T>
std::function<std::tuple<T>()> zip(const std::function<T()>& f) {
      return [f]() { return std::tuple<T>{f()}; };
//was return [f]() { return std::tuple{f()}; };
}

Rant...

Boiling down the issue in my source code to this was very nasty. For one, debugging these lambdas packed into std::functions is not fun. I tried g++'s -fsanitize=undefined, but this was not caught, although it catches the missing return type in this function:

template <typename T>
std::function<const T&()> constant_bad(const T& c) {
  return [c]() noexcept { return c; };
}

I filed a bug in GCC.

I would appreciate other comments and discussion about this issue! For example, would it be good, if possible, that compilers warn about this type of implicit conversion? Eg when implicitly converting a lambda returning T into a std::function<const T&()>.

Upvotes: 1

Pepijn Kramer
Pepijn Kramer

Reputation: 12849

#include <functional>
#include <type_traits>

template <typename T>
std::function<const T& ()> constant(const T& c)
{
    return [c]() noexcept -> const T& 
    { 
        // the captured variable is no longer a reference type!
        static_assert(std::is_same_v<decltype(c), std::remove_reference_t<const T&>>);
        return c; 
    };
}

int main()
{
    auto fn = constant(42);
    auto answer = fn();
}

Upvotes: 1

Related Questions