admin管理员组

文章数量:1356286

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
  • 3 AFAIK you are going to need to make your own function type that would not allow this. – NathanOliver Commented Mar 31 at 13:49
  • @NathanOliver I'm not sure that's possible. It's one avenue that I'm researching, but I'm starting to think C++ will always provide some way to inject the copy constructor if it is available. – srm Commented Mar 31 at 13:50
  • Why when you have const Thing& and you wish calling function would modify const? – Marek R Commented Mar 31 at 13:51
  • 1 @MarekR That's not the question. It's the inverse. I want to block a function that does modify from being assigned to the function. See my original question for more context. – srm Commented Mar 31 at 13:51
  • 1 You can replace std::function<void(const Thing&)> with std::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
 |  Show 1 more comment

2 Answers 2

Reset to default 5

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>> {});
}

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

本文标签: copy constructorC Best way to strengthen the type safety of assignment to stdfunctionStack Overflow