Calling Functions With Pre-Set Arguments in Modern C++

Published June 10, 2015 by Skidan Oleksii, posted by CodingMayhem
Do you see issues with this article? Let us know.
Advertisement

Introduction

A good fellow of mine gave me this interesting problem: pass a pre-stored set of arguments into a function without using std::function. I'd like to share with you my solution to this problem. Please, don't judge it strictly. I've never meant it to be perfect or finished for production use. Instead, I wanted to do everything as simple as possible, minimalistic but sufficient. Besides, there will be two solutions in this article. And one of them I like more than the other.

Implementation

Good Solution

The first way of solving the task exploits the fact that C++ already has a mechanism that allows us to capture variables. I talk about lambda functions. Of course, it would be great to use lambdas for this task. I'd show you a simple code snippet that has a lambda in it, just in case some of you are not familiar with C++14:

auto Variable = 1; 
auto Lambda = [Variable]() { 
	someFunction(Variable); 
}; 

A lambda function is being created in this call. This lambda captures the value of the variable named Variable. The object of the lambda function is being copied into a variable named Lambda. One can later call the lambda through that variable. A call to lambda will look like this:

Lambda(); 

It seems at first that the problem is solved, but really it's not. A lambda function can be returned from a function, a method or another lambda function, but it is hard to pass a lambda as an argument unless the receiver of that argument is a template.

auto makeLambda(int Variable) 
{ 
	return [Variable]() { someFunction(Variable); }; 
} 

auto Lambda = makeLambda(3); 

// What should be the signature of someOtherFunction()? 
someOtherFunction(Lambda); 

Lambda functions are objects of anonymous types. They have an internal structure which only the compiler knows of. Pure C++ (I mean C++ as a language without its libraries) does not give a programmer much operations at hand:

  • a lambda can be called;
  • a lambda can be converted to a function pointer, when the lambda is not capturing anything;
  • a lambda can be copied.

Frankly speaking, these operations are more than enough, because there are other mechanisms in the language which when combined give us a lot of flexibility. Let me share with you the solution to the problem which I ended up with.

#include <utility>
#include <vector>
#include <iostream>

template class SignalTraits; 

template class SignalTraits { public: using Result = R; }; 

template class Signal 
{ 
	public: 
		using Result = typename SignalTraits::Result; 
		
		template Signal(Callable Fn) : Storage(sizeof(Fn)) 
		{ 
			new (Storage.data()) Callable(std::move(Fn)); 
			Trampoline = [](Signal *S) -> Result { 
				auto CB = static_cast(static_cast(S->Storage.data())); 
				return (*CB)(); 
			}; 
		} 
		
		Result invoke() { return Trampoline(this); } 
		
	private: 
		Result (*Trampoline)(Signal *Self); 
		std::vector Storage; 
}; 

I'll explain briefly what is happening in that code snippet: the created non-capturing lambda function knows the type of Callable because it (the lambda) is being constructed in the template constructor. That's why the lambda is able to cast the data in Storage to the proper type. Really, that's it. All the hard lifting is done by the compiler. I consider this implementation to be simple and elegant.

Not So Good Solution

I like the other solution less, because it is filled with handmade stuff. And all that stuff is needed to capture variables, something C++ language already does for us out of the box. I don't want to spend a lot of words on this, so let me show you the implementation, which is large and clumsy.

#include <cstdarg>
#include <vector>
#include <cstdint>

template struct PromotedTraits { using Type = T; }; 
template <> struct PromotedTraits { using Type = int; }; 
template <> struct PromotedTraits { using Type = unsigned; }; 
template <> struct PromotedTraits { using Type = int; }; 
template <> struct PromotedTraits { using Type = unsigned; }; 
template <> struct PromotedTraits { using Type = double; }; 

template class StorageHelper; 

template class StorageHelper 
{ 
	public: 
		static void store(va_list &List, std::vector &Storage) 
		{ 
			using Type = typename PromotedTraits::Type; 
			union 
			{ 
				T Value; 
				std::uint8_t Bytes[sizeof(void *)]; 
			}; 
			
			Value = va_arg(List, Type); 
			for (auto B : Bytes) 
			{ 
				Storage.push_back(B); 
			} 
			
			StorageHelper::store(List, Storage); 
		} 
}; 

template <> class StorageHelper<> { public: static void store(...) {} }; 

template class InvokeHelper; 
template class InvokeHelper 
{ 
	public: 
		template static Result invoke(Result (*Fn)(Arguments...), Arguments... Args) 
		{ 
			return Fn(Args...); 
		} 
}; 

template class InvokeHelper 
{ 
	public: 
		template static Result invoke(...) { return {}; } 
}; 

struct Dummy; 
template class TypeAt { public: using Type = Dummy *; };
template class TypeAt { public: using Type = typename TypeAt<(Index - 1u), Types...>::Type; }; 
template class TypeAt<0u, T, Types...> { public: using Type = T; }; 

template class Signal; 
template class Signal 
{
	public: 
		using CFunction = Result(Arguments...); 
		
		Signal(CFunction *Delegate, Arguments... Values) : Delegate(Delegate) 
		{ 
			initialize(Delegate, Values...); 
		} 
		
		Result invoke() 
		{ 
			std::uintptr_t *Args = reinterpret_cast(Storage.data()); 
			Result R = {}; 
			
			using T0 = typename TypeAt<0u, Arguments...>::Type; 
			using T1 = typename TypeAt<0u, Arguments...>::Type; 
			
			// ... and so on. 
			
			switch (sizeof...(Arguments)) 
			{ 
				case 0u: return InvokeHelper<(0u == sizeof...(Arguments)), Arguments...>::template invoke(Delegate); 
				case 1u: return InvokeHelper<(1u == sizeof...(Arguments)), Arguments...>::template invoke(Delegate, (T0 &)Args[0]); 
				case 2u: return InvokeHelper<(2u == sizeof...(Arguments)), Arguments...>::template invoke(Delegate, (T0 &)Args[0], (T1 &)Args[1]); 
				// ... and so on. 
			} 
			
			return R; 
		} 
	private: 
		void initialize(CFunction *Delegate, ...) 
		{ 
			va_list List; 
			va_start(List, Delegate); 
			StorageHelper::store(List, Storage); 
			va_end(List); 
		} 
		
		CFunction *Delegate; 
		std::vector Storage; 
}; 

As for me, the only interesting things are the two helper classes: StorageHelper and InvokeHelper. The first class combines ellipsis with type list recursive algorithm to put arguments into Storage. The second class provides a type safe way of fetching arguments from that storage. And there's a tiny important detail: ellipsis promotes some types to others. I.e. float is promoted to double, char to int, short to int, etc.

Summary

I'd like to make a kind of a summary: I don't think the two solutions are perfect. They lack a lot and they try to reinvent the wheel. I'd say that the best way to pass pre-stored arguments into a function would be to use std::function + lambda. Though, as a mind exercise the problem is a lot of fun indeed. I hope you liked what you read and learned something useful for you. Thanks a lot for reading!

Article Update Log

9 June 2015: Initial release

Cancel Save
0 Likes 6 Comments

Comments

Khatharr

Maybe I'm missing something, but isn't this what std::bind is for?

June 17, 2015 06:43 AM
SiCrane

The code in the article seems to assume that lambdas have no-op destructors. This is not necessarily the case, especially when capturing complex types by value.

June 17, 2015 08:07 AM
CodingMayhem

The code in the article seems to assume that lambdas have no-op destructors. This is not necessarily the case, especially when capturing complex types by value.

Good point! In deed I assume no-op destructors just for simplicity, though it is incorrect in general. Thank you for your remark.

Maybe I'm missing something, but isn't this what std::bind is for?

Well, the whole point was not to use standard library. I'd say that I prefer to use standard library whenever possible. As for the std::bind: not really. In modern C++ you don't need to use std::bind in most cases, because lambdas will do that better than std::bind. I would like to hear a use case where std::bind provides substantial benefits over lambdas.

June 17, 2015 12:12 PM
Krohm

I think std::bind and lambdas are two very different things... I have difficulty getting the point of not using STL for this case.

June 18, 2015 02:35 PM
Dave Hunt

I'm not sure this article should be here, at least not in its current form.

The title sets one set of expectations - this article will demonstrate how to call functions with pre-set arguments using Modern C++.

However, the introduction sets a different set - this article will demonstrate a way to call functions with pre-set arguments without using all the tools available in Modern C++.

A proper article would meet the expectations set by the title. The content of this article is really just an interesting mind exercise and doesn't present a technique that you would actually want to use to solve the problem stated in the title.

June 19, 2015 06:24 PM
godmodder

Not using STL for this in "modern C++" makes this example a bit contrived imo.

June 20, 2015 10:30 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

Featured Tutorial

How do you call a function, given a pre-stored set of arguments? Read this article to find out ;-)

Advertisement

Other Tutorials by CodingMayhem

CodingMayhem has not posted any other tutorials. Encourage them to write more!
Advertisement