3

I'm using templates to explicitly declare and allow read access to specific data.

#include <type_traits>

template <typename T>
struct Access
{
    template <typename U>
    void Read()
    {
        static_assert(std::is_same_v<T, U>);
    }
};

Normally T would be a set of types, but I've simplified it here.

I'd like to ensure that any access that gets declared actually gets used. If a user declares Access<int> I want to check to see that there is a corresponding Read<int> somewhere.

Give that context, what I'm currently trying to do is detect whether Access<int>::Read<int> ever gets instantiated. Is this possible?

I tried using extern template to prevent implicit instantiations. The main problem here is that you have to explicitly write out every possible type at namespace scope. I don't see a way to do this systemically.

extern template void Access<int>::Read<int>();

int main()
{
    auto IsIntReadUsed = &Access<int>::Read<int>; // Linker error, yay!
    return 0;
}

Being able to detect this at compile time, link time, or run time is acceptable. The solution does not need to be portable across compilers. A solution that works on any single compiler is sufficient. Any C++ version is acceptable.

Here is a sandbox for experimenting https://godbolt.org/z/d5cco989v

// -----------------------------------------------------------
// Infrastructure

#include <type_traits>

template <typename T>
struct Access
{
    template <typename U>
    void Read()
    {
        static_assert(std::is_same_v<T, U>);
    }
};

int main()
{
    return 0;
}

// -----------------------------------------------------------
// User Code

using UserAccess = Access<int>;

void UserUpdate(UserAccess access)
{
    // Oh no, access.Read<int> is never used!

    (void) access;
}

// -----------------------------------------------------------
// Validation

template <typename TDeclared>
void ValidateAccess(Access<TDeclared>)
{
    // Write something here that can tell if Access<TDeclared>::Read<TDeclared> has been
    // used when this is called with UserAccess (i.e. ValidateAccess(UserAccess())).
    // This could also be implemented inside the Access class.
}
Adam
  • 1,122
  • 9
  • 21
  • It seems to me that any attempt to detect at compile whether or not a template was instantiated in a different translation unit, which may or may not've been compiled yet, is logically impossible. I can think of only some runtime checks that involve implementation-specific features like weak symbols. – Sam Varshavchik Mar 23 '22 at 19:47
  • It's in the same translation unit. Also, detecting at link time is possible, as with the `extern template` example above. A runtime solution is acceptable as well. – Adam Mar 23 '22 at 20:00
  • 1
    Seems that it is the compiler (or linkers) job to remove any extra instanciations that are not used. So not sure why you would want to try and do this manually. – Martin York Mar 23 '22 at 20:16

1 Answers1

2

Here's a link-time solution. Works on GCC, Clang, and MSVC.

One template (impl::Checker<T>) declares a friend function and calls it.

Another template (impl::Marker) defines that function. If it's not defined, the first class gets an undefined reference.

run on gcc.godbolt.org

#include <cstddef>
#include <type_traits>


namespace impl
{
    template <typename T>
    struct Checker
    {
        #if defined(__GNUC__) && !defined(__clang__)
        #pragma GCC diagnostic push 
        #pragma GCC diagnostic ignored "-Wnon-template-friend"
        #endif
        friend void adl_MarkerFunc(Checker<T>);
        #if defined(__GNUC__) && !defined(__clang__)
        #pragma GCC diagnostic pop
        #endif

        static std::nullptr_t Check()
        {
            adl_MarkerFunc(Checker<T>{});
            return nullptr;
        }

        inline static const std::nullptr_t check_var = Check();
        static constexpr std::integral_constant<decltype(&check_var), &check_var> use_check_var{};
    };


    template <typename T>
    struct Marker
    {
        friend void adl_MarkerFunc(Checker<T>) {}
    };
}


template <typename T, impl::Checker<T> = impl::Checker<T>{}>
struct Access
{
    template <typename U>
    void Read()
    {
        static_assert(std::is_same_v<T, U>);
        (void)impl::Marker<U>{};
    }
};


int main()
{
    Access<int> x;
    x.Read<int>();

    [[maybe_unused]] Access<float> y; // undefined reference to `impl::adl_MarkerFunc(impl::Checker<float>)'

    using T [[maybe_unused]] = Access<double>; // undefined reference to `impl::adl_MarkerFunc(impl::Checker<double>)'
}

Had to introduce a dummy template parameter to Access, since I couldn't think of any other way of detecting it being used in a using.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 2
    This is excellent. The secret sauce is that you can declare _and define_ a friend function inside a class. So you have a a way to define a namespace scoped function that two different classes agree on from inside one of those classes. What a useful piece of C++ trivia. – Adam Mar 24 '22 at 21:41
  • @Adam Yup! It lets you transfer information between classes in weird ways: [1](https://stackoverflow.com/a/70701479/2752075) [2](https://stackoverflow.com/a/67793557/2752075) – HolyBlackCat Mar 24 '22 at 21:46
  • ... Huh. @HolyBlackCat Is it just me, or does this completely obviate the need for the unsafe/runtime-checked cast that usually appears in the CRTP? I mean, C++23 does that too but some of us can't use it yet (*cough* CUDA C++ *cough* orz) – linkhyrule5 Aug 02 '23 at 08:32
  • Anyway I'm going to see if I can adapt this into a coherent framework for lifting inheritance to the template level. Thanks! – linkhyrule5 Aug 02 '23 at 08:34
  • @linkhyrule5 I don't understand what you're asking, can you show some code? What's unsafe about the cast in CRTP, and why would there be runtime checks? – HolyBlackCat Aug 02 '23 at 08:55
  • cppreference.com's page on the CRTP suggests the implementation `struct Base { void name() { (static_cast(this))->impl(); } };` prior to C++23. It then goes on to note that calling `name()` on a raw `Base` results in undefined behaviour, since Base doesn't actually define an `impl()` default, but this is not caught by the compiler due to the presence of `static_cast`. You could replace this with a well-defined runtime error by replacing `static_cast` with `dynamic_cast`, I believe, but ... ew :p. – linkhyrule5 Aug 02 '23 at 09:03
  • @linkhyrule5 Ah, gotcha. You can kinda guard against this by making base's constructors private and friending the template parameter. I don't see how my answer could help here. – HolyBlackCat Aug 02 '23 at 09:06
  • On the other hand, the pattern your post suggests -- which we might generalize into asserting that a given class has a friend member function and immediately calling it -- would achieve much the same effect as CRTP in that it "exposes an interface, and derived classes implement such interface", but a failure to _actually_ implement such interface (or accidentally calling the interface on a base class with no default) will result in a nice friendly _compile time_ error... I think? – linkhyrule5 Aug 02 '23 at 09:06
  • Thus we have achieved the same thing as CRTP, but without the unsafe cast -> undefined behavior, and without relying on new language features [s]that NVCC will predictably take an eternity to implement -_-[/s] – linkhyrule5 Aug 02 '23 at 09:08
  • @linkhyrule5 Ah, you're just talking about SFINAE? – HolyBlackCat Aug 02 '23 at 09:25
  • At least, a particularly clever use of it! – linkhyrule5 Aug 03 '23 at 03:14
  • 1
    Ah, wait, no, I see where I misunderstood. I misread this as basically foisting a function onto another class, at which point you could use that trick on the derived class given as a template parameter to `Base`; but you can only define a friend function if it's explicitly *not* someone else's member function, so that doesn't work. (To be expected, since that would be absolute hell for encapsulation.) Ah well. – linkhyrule5 Aug 03 '23 at 06:12