1

I'm using C++20 & I'm wondering if Concepts is at all possible to solve a situation like this.

Let's say I have a function:

template <typename F>
void doSomething(F f) {
  f();
}

doSomething accepts a callable objects (technically lambdas but shouldn't matter I think) & I want to ensure that no member variables of F have a type T. For example, I want:

BadType t;
int x = ...;
double y = ...;
doSomething([t = kj::mv(t), x, y] { ... }); // I want a compile error
doSomething([x, y] { ... }); // No error.

Similarly, I want to validate the arguments of the callable in a similar way:

doSomething([x, y](BadType t) { ... }); // I want a compile error
doSomething([x, y](std::vector<int> c) { ... }); // No compile error

I don't know if this complicates things, but technically BadType itself is a templated type (I want to disable all instances regardless of the template value).

An acceptable answer could be one that provides an example, but I'm also happy with good tutorials that someone feels I should be could be able to cobble together to accomplish. I'm very experienced with C++ & template meta-programming, but concepts feel like a totally alien thing at the moment. Of course, if this isn't possible, then happy to accept such an answer too.

This feels like something that the Reflection TS would be perfect for, but alas clang doesn't have this in an accessible way even in v13 (& I think similarly the GCC work is in a non-mainline branch still). I've explored various static reflection libraries for C++17, but they all suffer from needing to modify the type, which isn't possible here since I'm introspecting a lambda (and BadType is a type defined in a 3p library although that shouldn't matter).

I suspect the answer must be no since each lambda I pass in will have an arbitrary set of names for the variables that get captured & the only examples I've seen of concepts trying to enforce the type of a member variable require a single known variable name, but maybe this is an interesting challenge for someone who's a Concepts master.

EricSchaefer
  • 25,272
  • 21
  • 67
  • 103
Vitali
  • 3,411
  • 2
  • 24
  • 25
  • 1
    "ensure that no member variables of F have a type T" This is utterly impossible. "validate the arguments of the callable" It isn't quite clear what exactly you want to validate. You are calling your callable with no arguments, so a callable that accepts any arguments will cause a compilation error. – n. m. could be an AI Apr 14 '21 at 18:22
  • Read this on why to forward functors: https://stackoverflow.com/questions/24779910/why-use-a-perfectly-forwarded-value-a-functor – David Bien Apr 14 '21 at 18:39
  • @n.'pronouns'm.: *""ensure that no member variables of F have a type T" This is utterly impossible"*. Wrong. See my answer. – Vittorio Romeo Apr 14 '21 at 18:42
  • 3
    Why do you want this? I'm having a hard time coming up with a reason to do this. – Kevin Apr 14 '21 at 18:43
  • 2
    @VittorioRomeo a lambda is not a simple struct nor an aggregate. – n. m. could be an AI Apr 14 '21 at 18:44

2 Answers2

1

You can easily check if a simple struct/aggregate T contains any field with type TType using boost::pfr:

#include <boost/pfr.hpp>

template <typename TType, typename T>
[[nodiscard]] constexpr bool has_any_data_member_of_type() noexcept
{
    return []<std::size_t... Is>(std::index_sequence<Is...>)
    {
        return (std::is_same_v<boost::pfr::tuple_element_t<Is, T>, TType> || ...);
    }(std::make_index_sequence<boost::pfr::tuple_size_v<T>>{});
}

struct a { int i; float f; char c; };
struct b { int i;          char c; };

static_assert( has_any_data_member_of_type<float, a>());
static_assert(!has_any_data_member_of_type<float, b>());

live example on godbolt.org


You can then easily define a concept for that:

template <typename T, typename TType>
concept having_any_data_member_of_type = has_any_data_member_of_type<TType, T>();

Unfortunately, since lambda expressions are not aggregates, you won't be able to use them with has_any_data_member_of_type. However, custom callable objects work:

struct my_callable
{
    float f;
    float operator()() { return f; }
};

void f0(having_any_data_member_of_type<float> auto f)
{
    (void) f();
}

int main()
{
    f0(my_callable{}); // OK
}
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • That's really neat & useful link. Unfortunately I need a solution for lambdas. Custom callable won't work in this case (large codebase, so a manual conversion will be too time-consuming unless I'm missing some way to do this). – Vitali Apr 14 '21 at 22:03
  • I don't believe there's a way to emulate reflection on lambda captures in C++20. – Vittorio Romeo Apr 14 '21 at 23:28
0

If you are happy with checking only a direct nesting of an object inside a simple struct/aggregates see Vittorio's answer.

If your goal is to prohibit capturing an object of some type inside callable objects (for example, prohibiting capturing a reference in the asynchronous callback or prohibiting capturing some object that should not be accessed from callback) then it is impossible in the general case even with reflection TS.

Simple counter example:

BadType t;
doSomething([t = std::any(kj::mv(t))]{...}); // Even reflection TS could not do anything with this.

Another one:

struct Fun {
    std::any v;
    void operator()() {
       ....
    }
}

Fun f = ...
doSomething(std::move(f)); // Event more powerful reflection than reflection TS (reflection with statements reflection) could not do anything with this.

Even if nesting in multiple layers of objects could be taken into account any kind of type-erasure simply could not.

Serikov
  • 1,159
  • 8
  • 15
  • Sure, I don't happen to have any instances of type erasure to worry about in the use-case I have so that's an extreme counter-example (i.e. reflection TS should work if it were available). Just a middle ground where I don't have simple struct/aggregates & I need to enforce this on lambdas that have a known current pattern I want to enforce. Maybe semgrep will be a good middle ground. – Vitali Apr 23 '21 at 23:10