Inferring (at compile-time) which members are read inside method

81 Views Asked by At

Consider this code:

#include <type_traits>
#include <tuple>

// Random structs that follow this pattern
struct X { int         x; using Type = int; };
struct Y { float       y; using Type = float; };
struct Z { const char* z; using Type = const char*; };
struct W { double      w; using Type = double; };

// Struct composed via inheritance
struct S : X, Y, Z, W
{
    // This method reads `x` and nothing else
    Y::Type MethodY() const
    {
        // Some random logic
        return static_cast<float>(x) * 10.0F;
    }

    // This methods reads `x`, `y` and `z`
    W::Type MethodW() const
    {
        // Some random logic again
        return z ? static_cast<double>(y) : static_cast<double>(x);
    }

    // Imagine there may be more such methods
    // ...
};

template<auto Method>
using WhatMethodReads = std::tuple</* List of bases of `S` that are used in `Method` */>;

// Goal:
static_assert(std::is_same_v<WhatMethodReads<&S::MethodY>, std::tuple<X>>);
static_assert(std::is_same_v<WhatMethodReads<&S::MethodW>, std::tuple<X, Y, Z>>);

int main() { }

Problem: basically, given the method, I want to infer (at compile-time) which particular members the method reads inside its body.

In the sample particularly, I construct the type via multiple inheritance, so each member is strongly tied to a dedicated struct; but if there are solutions that work with other way of composition, those are welcomed as well.

The method may be a free function instead that takes a const ref to S, it doesn't really matter.

What I tried

I tried to test whether method compiles or not while changing public inheritance to private (thus hiding members) or by removing those bases entirely using template metaprogramming. I was expecting SFINAE to pick alternative specialization when method doesn't compile without a particular base (like, I remove, base Y from A: MethodW body is now ill-formed - it means that MethodW does depend on Y so it goes into resulting tuple of WhatMethodReads<&S::MethodW>; on the other hand MethodY is still well-formed when I remove that same Y so Y won't go into WhatMethodReads<&S::MethodY>).

The problem is that when compiler sees access to private member of base (or non-existing member), it just stops the compilation with error instead of applying SFINAE rules.

So, this approach boils down to implementing a legal way to get bool constexpr value that tells whether function body is well-formed ot not. This would be sufficient to solve my problem.

Sample of implementation attempt:

#include <type_traits>
#include <iostream>

template<class T>
auto Function()
{
    // Imagine usage is much more complicated
    // and assume that `requires { T + T; }`
    // doesn't suffice for me. I want substitution
    // in other place to fail based on whether this
    // function is well-formed or not.
    return T{} + T{};
}

template<class T>
struct IsFunctionLegal : std::false_type
{ };

template<class T>
    // So, one of the attempts was to declare that
    // function return type is `auto` and then
    // use `decltype` in some way to infer
    // whether function body is legal or not.
    // But it results in compile error instead of SFINAE.
    requires requires { std::is_same_v<decltype(Function<T>()), T>; }
struct IsFunctionLegal<T> : std::true_type
{ };

int main()
{
    std::cout << IsFunctionLegal<int>::value;  // fine, returns true
    std::cout << IsFunctionLegal<void>::value; // error instead of false
}

Error:

<source>: In instantiation of 'auto Function() [with T = void]':
<source>:25:60:   required by substitution of 'template<class T>  requires requires{is_same_v<decltype((Function<T>)()), T>;} struct IsFunctionLegal<T> [with T = void]'
<source>:32:39:   required from here
<source>:12:16: error: invalid operands of types 'void' and 'void' to binary 'operator+'
   12 |     return T{} + T{};
      |            ~~~~^~~~~
Build failed
0

There are 0 best solutions below