A bit of street magic, or how to statically determine if a function is being called

Recently I was asked a problem, in the discussion it all boiled down to the following: – there is an object, it has methods. Each method / s requires loading some kind of logic at runtime. We want to know exactly what methods were called, and then request loading only the necessary functionality at runtime.

Disclaimer

I will immediately anticipate a lot of comments on the topic “but it is not defined in the standard”, “but my HZZ is 5”, “but in my team there are C with classes” and so on. Therefore, if all (any of) this concerns you, you do not need to apply anything described here in your practice. This can be used by those who understand what he is doing and what the consequences are.

My understanding of C ++ is very specific and I do not recommend anyone to follow what I am following without any reason. And I also ask you to refrain from unnecessarily imposing your visions on me.

Wherever I talk about C ++, or about any of its behavior – I always mean gnu ++, and the behavior of its (gnu ++) implementations. If I continue to write, it will manifest itself more and more.

There is no need to come and demonstrate your knowledge of the bureaucracy, starting religious wars here.

Focus is based on a few basic properties

struct a {
  
  static inline auto x = 123;//статическая инициализация происходит
  //до старта программы, которая main
};

struct b {
  
  static bool f() {
    return true;
  }
  static inline auto x = f();
};

Here static inline auto x = f() – initialization depends on the result, so in the process it is necessary to call the function. Any side effects in the function will be executed even though there return true is the basic semantics of the language.

thus a similar program:

#include<cstdio>

struct c {
  static bool f() {
    fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
    return true;
  }
  static inline auto x = f();
};


int main() {
  fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
}

will output:

static bool c::f()
int main()

The most obvious usage pattern is

template<typename> struct plugin {
  
  static bool f() {
    fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
    return true;
  }
  static inline auto x = f();
};

struct my_plugin: plugin<my_plugin> {};//подобное работать не будет

The rule here is simple – any entities inside a polymorphic / template context are instantiated lazily, i.e. only when contacting.

template<typename T> struct test {
  auto f() {
    T x = "";
  }
};

test<int> _;//никакой ошибки не будет.

This allows you not to pay for what we don’t use.

let’s take a similar example:

template<auto x> struct integral_constant {
  constexpr operator auto() const { return x; }
  
  template<auto y> constexpr integral_constant<x % y> operator%(integral_constant<y>) const { return {};}
  template<auto y> constexpr integral_constant<x + y> operator+(integral_constant<y>) const { return {};}
};

static_assert(integral_constant<1.>{} + integral_constant<2.>{} == 3.); – no need to implement what we do not use. In this case %

Thus, if in integral_constant есть operator%, а параметризуем мы её(integral_constant) double для которой % не определена - всё работает

Likewise with the rest:

static_assert(integral_constant<3>{} % integral_constant<2>{} == 1);


constexpr auto test_mod(auto a, auto b) {
  return requires {
    a % b;
  };
}


static_assert(!test_mod(integral_constant<1.>{}, integral_constant<2.>{}));
static_assert(test_mod(integral_constant<1>{}, integral_constant<2>{}));

There is an important point – sfinae. If we want from such methods the same behavior as the default implementation of operators, let’s say for the same double – we need to move all dependencies into the signature. In this case, I took them outтак: integral_constant<x % y> - мы не сможем инстанцировать эту сигнатуру если между x и y не определено %. Если же мы попытаемся перенести это в теле - sfinae будет пробиваться.

Этот механизм предполагает, что из факта возможно инстанцирования сигнатуры следует возможность инстанцирования тела. +/- это работало когда-то, но не сейчас. Здесь нужно будет побороться с наследием.

The solution to the problem above is obvious – you need to use the field outside the polymorphic / template context

struct my_plugin2: plugin<my_plugin2> {
  static inline auto x = plugin::x;//сайд-эффектом этой инициализации является вызов plugin<my_plugin2>::f.
  //То, что нам и нужно
};

It remains only to combine everything together

template<auto tag> struct registry {
  static auto push() {
    fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
    return true;
  }
  static inline auto x = push();
};

#undef NDEBUG
#include<cassert>

template<typename = void> struct object {
  void a() {
    assert(registry<&object::a>::x);
  }
  void b() {
    assert(registry<&object::b>::x);
  }
};

void test_registry() {
  object o;
  o.a();//static auto registry<<anonymous> >::push() [with auto <anonymous> = &object<void>::a]
  //o.b();//static auto registry<<anonymous> >::push() [with auto <anonymous> = &object<void>::b]
}

We can also use the static_assert property, which requires converting an expression to constexpr but does not require the entire expression as constexpr

constexpr integral_constant<true> true_;

template<auto tag> struct registryv2 {
  static auto push() {
    fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
    return true_;
  }
  static inline auto x = push();
};

template<typename = void> struct objectv2 {
  void a() {
    static_assert(registryv2<&objectv2::a>::x);
  }
  void b() {
    static_assert(registryv2<&objectv2::b>::x);
  }
};

void test_registryv2() {
  objectv2 o;
  o.a();//static auto registryv2<<anonymous> >::push() [with auto <anonymous> = &objectv2<void>::a]
//   o.b();//static auto registryv2<<anonymous> >::push() [with auto <anonymous> = &objectv2<void>::b]
}

template<typename T = void> void f() {
  static_assert(registryv2<&f<T>>::x);
}

void test_f() {
//   f();//static auto registryv2<tag>::push() [with auto tag = f<>]
}




int main() {
  fprintf(stderr, "%sn", __PRETTY_FUNCTION__);
}

It is very simple to forbid putting some arguments into a template:

using private_unique_type = decltype([]{});

template<typename T = private_unique_type> void f2() {
  static_assert(__is_same(T, private_unique_type));
  static_assert(registryv2<&f2<T>>::x);
}


void test_f2() {
//   f2();
}

Full text to play with

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *