Search code examples
c++lambdahigher-order-functionstemplate-argument-deduction

C++ high order lambda cannot deduce returned lambda type


I've got an assignment to create practically generic class that gets in some kind of input source of integers and some kind of consumer that consumes those inputs. I've succeeded to make this work, but it is uglier than I want it to be. So this is part of my solution that works.

This is an interface that my input sources implement:

 class InputSource {
   public:
      virtual const InputSource& operator<<(int& num) const = 0;
      virtual ~InputSource()=default;
 };

This is one implementation that gets input from the keyboard:

class KeyboardInput : public InputSource {
   public:
      KeyboardInput()=default;
      virtual ~KeyboardInput()=default;

   virtual const InputSource& operator<<(int& num) const override {
      std::cin >> num;
      return *this;
   }
};

And this is my implementation of number sequence class that takes in some input source and takes in an action which is an std::function that operates on numbers that are given from the input source

class NumberSequence {

const InputSource &input_source;
const std::function<void(int)> &action;

int next_num() {int num; input_source<<num; return (num<0 ? -1 : num);} // -1 if no morenumbers

public:
   NumberSequence(const InputSource &input_source, const std::function<void(int)> &action) : input_source(input_source), action(action) {}

   void start_action() {
       int num;
       do {
          action(num = next_num());
          if(num == -1) break;
          std::this_thread::sleep_for(std::chrono::seconds(1));
       } while(true);
   }
 };

Meat of this class is start_action member function that gets input from the given input source and than calls action with that number and waits for 1 sec and does that until input source gives -1, simple as that.

So now I wrote an implementation of one action that outputs those numbers to file, but not as a class but as a lambda like this:

static auto write_to_file_action = [](std::ofstream& output_file) {

     return [&output_file](int num){
       if(num == -1) return;
       using namespace std::chrono;

       time_point<system_clock> now = system_clock::now();
       std::time_t time = system_clock::to_time_t(now);
       output_file << num <<"\t"<< std::ctime(&time) << std::endl;
    };
};

This lambda takes in an std::ofstream to the file to which I'm outputting the numbers plus it adds time, but it's not really relevant(part with time). So the use of this in my main function is something like this:

int main(void) {
   KeyboardInput input_source;

   std::ofstream output_file("output_file.txt");
   NumberSequence num_seq(input_source, write_to_file_action(output_file));

   num_seq.start_action();

   return 0;
}

Like I said this works but I would like to have something like this:

int main(void) {
   KeyboardInput input_source;

   NumberSequence num_seq(input_source, write_to_file_action("output_file.txt"));

   num_seq.start_action();

   return 0;
}

Which looks so simple but I have a hard time implementing it. I've tried implementing the write_to_file_action like this:

static auto write_to_file_action = [](const char* file_name) {

    std::ofstream output_file(file_name);

    return [output_file = std::move(output_file)](int num) mutable {
       if(num == -1) return;
       using namespace std::chrono;

       time_point<system_clock> now = system_clock::now();
       std::time_t time = system_clock::to_time_t(now);
       output_file << num <<"\t"<< std::ctime(&time) << std::endl;
    };
};

But then I get compilation error which basically says that this will not work because my NumberSequence class wants an std::function but std::function must be copyable and this is not my case. In the clojure of my inner lambda i have an std::ofstream which is not copyable.

So I've tried to template my NumberSequence class like this:

template<typename Func>
class NumberSequence {

    const InputSource &input_source;
    const Func action;

    int next_num() {int num; input_source<<num; return (num<0 ? -1 : num);} // -1 if no more numbers

    public:
       NumberSequence(const InputSource &input_source, Func &&action) 
           : input_source(input_source), action(std::move(action)) {}

    void start_action() {
        int num;
        do {
           action(num = next_num());
           if(num == -1) break;
           std::this_thread::sleep_for(std::chrono::seconds(1));
        } while(true);
    }
};

This won't compile but now it's not compiling because it says that is missing an template argument before num_seq variable in main(it can deduce it), so I can do something like this:

int main(void) {
    KeyboardInput input_source;

    auto lambda = write_to_file_action("output_file.txt");
    NumberSequence<decltype(lambda)> num_seq(input_source, lambda);

    num_seq.start_action();

    return 0;
}

And I also need to make another constructor in my NumberSequence that takes in a normal reference to action and not rvalue reference. Like I sad this works but I would like to remove this explicit template instatitation. I don't know if this is possible but I looks that it could be. If someone can explain why it can't deduce the type implicitly because I don't get it. Thank you.

P.S. Sorry for long post it's my first time posting and I wanted to capture the whole context of my problem.


Solution

  • Example of lambda auto deduction methods in C++14. One generates template function that deduces the type and returns appropriately created class template.

    #include <iostream>
    #include <memory>
    
    using namespace std;
    
        template<typename PFunc>
        class   CProcGuard
        {
        public:
            CProcGuard(PFunc&& f):mFunc(std::move(f)){};
            CProcGuard(CProcGuard&&) = default;
            ~CProcGuard()
            {
                mFunc();
            }
    
        private:
            PFunc mFunc;
        };
    
        template<typename PFunc>
        auto    ExecuteOnExit(PFunc&& f) -> CProcGuard<PFunc>
        {
            return CProcGuard<PFunc>(std::move(f)); // god bless RVO
        }
    
    int main()
    {
        std::unique_ptr<int> nonMovable =make_unique<int>(5);
    
        auto exitGuard = ExecuteOnExit([nm = std::move(nonMovable)]()
        {
            cout<<"Hello World " << *nm;
        });
    
        return 0;
    }
    

    Your second option is to make the lambda movable via std::shared_ptr. Something like that:

    static auto write_to_file_action = [](const char* file_name) {
    
        auto ptr_output_file = std::make_shared<std::ofstream>(file_name);
    
        return [ptr_output_file = std::move(ptr_output_file)](int num) mutable {
           if(num == -1) return;
           using namespace std::chrono;
    
           time_point<system_clock> now = system_clock::now();
           std::time_t time = system_clock::to_time_t(now);
           *ptr_output_file  << num <<"\t"<< std::ctime(&time) << std::endl;
        };
    };
    

    Kinda dumb and inefficient but it works.

    Note: there is no point in making a static lambda write_to_file_action - just make a regular function - it probably confuses some people.