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