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;
}
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;
}
Share
Improve this question
edited Mar 31 at 13:48
srm
asked Mar 31 at 13:40
srmsrm
3,26618 silver badges33 bronze badges
6
|
Show 1 more comment
2 Answers
Reset to default 5Instead 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>> {});
}
Demo
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))>()) {
Demo
Wrapping std::function
in your own type would allow some type checking.
Something like
namespace detail
{
template <typename Ret, typename... Args, typename C>
std::true_type match_operator(Ret (C::*)(Args...) const);
template <typename Ret, typename... Args, typename C>
std::true_type match_operator(Ret (C::*)(Args...));
template <typename T>
std::false_type match_operator(T*);
template <typename T, typename Ret, typename... Args>
concept has_call_operator = static_cast<bool>(decltype(detail::match_operator<Ret, Args...>(&T::operator())){});
}
template <typename Sig>
class strict_function;
template <typename Ret, typename... Args>
class strict_function<Ret(Args...)>
{
public:
strict_function(Ret(*f)(Args...)) : m_f(f) {}
template <typename C>
strict_function(C c) requires detail::has_call_operator<C, Ret, Args...> : m_f(c) {}
Ret operator()(Args... args)
{
return m_f(std::forward<Args>(args)...);
}
private:
std::function<Ret(Args...)> m_f;
};
Demo
There are some caveats (at least with my implementation), classes with overloaded operator()
won't work, and missing some constructors/methods to better mimic std::function
function
type that would not allow this. – NathanOliver Commented Mar 31 at 13:49const Thing&
and you wish calling function would modify const? – Marek R Commented Mar 31 at 13:51std::function<void(const Thing&)>
withstd::function<void(nocopy<Thing>)>
where nocopy<Thing> can be implicitly constructed with a const Thing& but not a Thing&& or Thing? – Jeff Garrett Commented Mar 31 at 16:07