Summary: Earlier, I asked this question:
Why does C++ allow std::function assignments to imply an object copy?
Now that I understand why the code works the way it does, I would like to know how to fix it. If a parameter is const X&
, I do not want functions that take just X
to be assignable. Is there some sort of signature change or wrapper class around X or other technique that would guarantee that only functions that match a typedef exactly can be assigned to std::function
of that typedef?
I know I could remove the copy constructor from the parameter class, but we need the copy constructor in other parts of the code. I am hoping to avoid a deeper refactoring of adding a formal "CopyMe" method that has to be added everywhere else. I want to fix the std::function assignment, if possible.
Details: The C++ type std::function does not enforce strict assignment with regard to "const" and "&" parameters. I can have a typedef whose parameter is const X&
like this:
typedef std::function<void(const Thing&)> ConstByRefFunction;
but it allows assignment of a function that is by value like this:
void DoThingByValue(Thing event);
ConstByRefFunction func = DoThingByValue; // LEGAL!
For a full example with context, see the code below. The assignment injects a copy constructor to make the call.
I do not want that to be legal. I am trying to write C++ code that tightly controls when copy constructors get invoked and enforce across my application that all functions assigned to a particular callback follow the exact same signature, without allowing the deviation shown above. I have a couple of ideas that I may try to strengthen this (mostly involving wrapper classes for type Thing
), but I am wondering if someone has already figured out how to force some type safety. Or maybe you've already proved out that it is impossible, and I just need to train devs and make sure we are doing strict inspection in pull requests. I hate that answer, but I'll take it if that's just the way C++ works.
Below is the full example. Note the line marked with "I WISH THIS WOULD BREAK IN COMPILE":
#include <memory>
#include <functional>
#include <iostream>
class Thing {
public:
Thing(int count) : count_(count) {}
int count_;
};
typedef std::function<void(const Thing&)> ConstByRefFunction;
void DoThingByValue(Thing event) { event.count_ += 5; }
int main() {
Thing thing(95);
ConstByRefFunction func = DoThingByValue; // I WISH THIS WOULD BREAK IN COMPILE
// The following line copies thing even though anyone looking at
// the signature of ConstByRefFunction would expect it not to copy.
func(thing);
std::cout << thing.count_ << std::endl; // still 95
return 0;
}
Instead of providing your own std::function
-like implementation enforcing your requirements, you could have a slightly different approach by using a make_function
helper that takes as template parameter the type you want to use (ConstByRefFunction
in your example) and the function to be encapsulated. make_function
will return the correct std::function
object if the check is ok, otherwise a compilation error is generated. Example of use:
struct Thing { int count; };
void DoThingByValue (Thing event) {}
void DoThingByConstRef (Thing const& event) {}
using ConstByRefFunction = std::function<void(Thing const&)>;
int main()
{
// Compilation error here ("f1 has incomplete type")
// auto f1 = make_function<ConstByRefFunction>(DoThingByValue);
// This one compiles and f2 is a std::function<void(Thing const&)> object
auto f2 = make_function<ConstByRefFunction>(DoThingByConstRef);
f2 (Thing {95});
return 0;
}
The definition of make_function
is
template<typename T, typename FCT>
[[nodiscard]]auto make_function (FCT fct)
{
if constexpr (signatures_match<T, decltype(std::function(fct))> () ) {
return T(fct);
}
// if the two signatures differ, we return nothing on purpose.
}
which compares the two signatures with the signatures_match
function. If the constexpr
check fails, nothing is returned and then one gets a compilation error such as variable has incomplete type 'void'
when trying to assign the result of make_function
to an object.
signatures_match
can be implemented by some textbook type traits:
template<typename T> struct function_traits;
template<typename R, typename ...Args>
struct function_traits<std::function<R(Args...)>> {
using targs = std::tuple<Args...>;
};
template<typename F1, typename F2>
constexpr bool signatures_match ()
{
// we get the signatures of the two provided std::function F1 and F2
using sig1 = typename function_traits<F1>::targs;
using sig2 = typename function_traits<F2>::targs;
if (std::tuple_size_v<sig1> != std::tuple_size_v<sig2>) { return false; }
// we check that the two signatures have the same types.
return [&] <auto...Is> (std::index_sequence<Is...>) {
return std::conjunction_v <
std::is_same <
std::tuple_element_t<Is,sig1>,
std::tuple_element_t<Is,sig2>
>...
>;
} (std::make_index_sequence<std::tuple_size_v<sig1>> {});
}
It compiles with c++20
but modifications are quite simple to make it compile with c++17
.
Update
If you want to have a little bit more specific error message, it is also possible to simply rely on concepts:
template<typename T, typename FCT>
[[nodiscard]]auto make_function (FCT fct)
requires (signatures_match<T,decltype(std::function(fct))>()) {
return T(fct);
}
For instance, clang would produce the following error message:
<source>:49:15: error: no matching function for call to 'make_function'
49 | auto f1 = make_function <ConstByRefFunction>(DoThingByValue);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:40:19: note: candidate template ignored: constraints not satisfied [with T = ConstByRefFunction, FCT = void (*)(Thing)]
40 | [[nodiscard]]auto make_function (FCT fct)
| ^
<source>:41:11: note: because 'signatures_match<std::function<void (const Thing &)>, decltype((std::function<void (Thing)>)(fct))>()' evaluated to false
41 | requires (signatures_match<T,decltype(std::function(fct))>()) {