C++: static assert that functor's argument is const reference

Viewed 336

I'm writing a template function in C++17 which accepts a functor F as argument and I want to restrict the passed in functor to have only one constant reference argument, where T can be any type.

for example:

template <class T> struct my_struct{
    std::vector<T> collection;
    template <class F> std::vector<T> func(F f){
        static_assert(
               // CONDITION HERE!,
               "f's argument is not const reference"
            );

        std::vector<T> ret;

        std::copy_if(
                std::make_move_iterator(this->collection.begin()),
                std::make_move_iterator(this->collection.end()),
                std::inserter(ret, ret.end()),
                f
            );

        return ret;
    }
};

Obviously, in case f is [](auto v){return true;} the resulting vector returned from func will have empty elements (because those are moved before adding to resulting container). So, I need to restrict possible input functors to [](const auto& v){}.

I have tried something like this:

static_assert(
        std::is_invocable_v<F, const T&>,
        "f's argument is not const reference"
    );

But then func([](auto v){}) does not trigger the assertion, because T is copyable in my case. The func([](auto& v){}) also passes the test, because auto can be const T.

But I need to limit possible lambdas to be func([](const auto& v){}).

3 Answers

You might write traits (with its limitations), something like:

template <typename Sig> struct callable_traits;

template <typename Ret, typename ...Args>
struct callable_traits<Ret(*)(Args...)>
{
    using args = std::tuple<Args...>;
};
// add specialization for C-ellipsis too

template <typename Ret, class C, typename ...Args>
struct callable_traits<Ret(C::*)(Args...) const>
{
    using args = std::tuple<Args...>;
};
// add specialization for variant with C-ellipsis, cv-qualifier, ref-qualifier

template <class C>
struct callable_traits<C> : callable_traits<&C::operator()>{};

Limitation of the traits: doesn't handle templated operator() (as for generic lambda), overloaded operator().

And then

template <class T> struct my_struct{
    template <class F> void func(F f){
        static_assert(
               std::is_same_v<std::tuple<const T&>, typename callable_traits<F>::args>,
               "f's argument is not const reference"
            );

        // here goes some code which can possibly call f with rvalue
        // reference argument, so I want to avoid situation when the
        // argument object is moved or modified. I don't have control
        // over this code because it an STL algorithm.
    }
};

I could be misunderstanding what you're trying to do but, as I read it, you want to accept a callable, then pass some argument to it with a guarantee that the argument cannot be changed (you don't want someone to accept the argument as a non-const l-value reference or an r-value reference. If so, then std::is_invocable should be enough:

#include <type_traits> // for std::is_invocable
#include <functional> // for std::invoke

template <class parameter_t> struct my_struct {
    template <typename callable_t>
    void function(callable_t const &callable) requires (
        std::is_invocable<callable_t, parameter_t const &>::value
    ) {
        // . . .
        std::invoke(callable, parameter_t{});
        // . . .
    }
};

Then:

int main() {
    my_struct<int>{}.function([](int const&){}); // Fine
    my_struct<int>{}.function([](int){}); // Fine, a copy of the parameter is made when the lambda is invoked.
    my_struct<int>{}.function([](int &){}); // error: no matching member function for call to 'function'
    my_struct<int>{}.function([](int &&){}); // error: no matching member function for call to 'function'
}

(You can play around with it here)

A possible problem is that this method does allow a copy to be made, but if the main goal is to protect the variable you hold from changes this should be good enough.

P. S. I know I used c++20 requires clause as a way of future-proofing the answer, but it should be trivial to convert it to if constexpr, static_assert or any other way you prefer.

I finally managed to achieve it in the following way:


template <class T> struct my_struct{
    std::vector<T> collection;

    struct noncopyable_value_type : T{
        noncopyable_value_type(const noncopyable_value_type&) = delete;
        noncopyable_value_type& operator=(const noncopyable_value_type&) = delete;
    };

    template <class F> std::vector<T> func(F f){
        static_assert(
               std::is_invocable_v<F, noncopyable_value_type>,
               "f's argument must be const reference"
            );

        std::vector<T> ret;

        std::copy_if(
                std::make_move_iterator(this->collection.begin()),
                std::make_move_iterator(this->collection.end()),
                std::inserter(ret, ret.end()),
                f
            );

        return ret;
    }
};

But still, the problem here is that it only works with generic lambdas.

Related