Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I detect at compile time "function arguments" that are compile-time constants

Can I detect at compile time whether "function arguments"1 are compile-time constants?

For example a function print(int i) that can print "constant 5" if called as print(5) but "non-constant 5" if called as print(i) where i is some non-constant variable. In particular, in the "is constant" branch I should be able to treat i as a constexpr, including using it for template arguments, etc.

Macro tricks, template meta-programming and SFINAE tricks are all OK. Ideally it is portable, but solutions that are compiler-specific are better than nothing.

It's OK if there are "false negatives" - i.e., if constant values are sometimes detected as non-constant (e.g., when certain optimizations are disabled).

Bonus points if the solution can detect when constant values are indirectly passed to the function (e.g., when a constant value is passed to an intermediate function that calls print and which is subsequently inlined exposing the constant to print). This last behavior is evidently optimization dependent.

Double bonus points if it naturally extends to multiple arguments.

If one could have overloaded versions of functions with and without constexpr arguments this would presumably be straightforward, but you can't.


1 I'm putting "function arguments" in quotes here because the solution doesn't strictly require detecting this state within a function (or at the caller/callee boundary with special arguments) - it just has to appear to the caller like a function but macros or other tricks like a static object with operator() etc could be used.

like image 584
BeeOnRope Avatar asked Dec 06 '17 00:12

BeeOnRope


People also ask

In which the response to a function is determined at the compile time?

In static polymorphism, the response to a function is determined at the compile time. In dynamic polymorphism, it is decided at run-time. The linking of a function with an object during compile time is called early binding.

What is used for a function to execute at compile time?

In computing, compile-time function execution (or compile time function evaluation, or general constant expressions) is the ability of a compiler, that would normally compile a function to machine code and execute it at run time, to execute the function at compile time.

What is compile time parameter?

Compile time parameters are parameters utilized during the compilation of a template. These parameters can only be used via the SparkleFormation library. It is for this reason that compile time parameters are generally discouraged from use.

What is known at compile time?

In computer science, compile time (or compile-time) describes the time window during which a computer program is compiled. The term is used as an adjective to describe concepts related to the context of program compilation, as opposed to concepts related to the context of program execution (runtime).


2 Answers

it doesn't have to be a plain function void print(int i) - it could be a function-like macro that does some magic on its arguments and calls a different function depending on whether it is a constant, or it could be some template magic

"function-like macro" you said?

Well... first of all I have to warn you that C-style function-like macro are dangerous. Distilled evil, IMHO.

Said this, if you really accept a macro based solution, I suppose that combining it with constexpr methods, a template struct, static local variables and SFINAE...

If you define the following template PrintStruct struct

template <typename T>
struct PrintStruct
 {
   template <bool>
   static void func (...) 
    { std::cout << "func non-const: " << T::func(true) << std::endl; }

   template <bool b, int I = T::func(b)>
   static void func (int) 
    { std::cout << "func const:     " << I << std::endl; }
 };

and the following C-style function-like macro, that define a foo local struct and pass it, as template argument, to PrintStruct to activate SFINAE to select the desired func() (and call func(), obviously) [EDIT: macro improved by jxh to make it expand as a statement; thanks!] [EDIT 2: macro modified, following an observation of the OP, to accept expressions]

#define Print(i)                          \
[&]()                                     \
 {                                        \
   static int const printLocalVar { i };  \
                                          \
   struct local_foo                       \
    {                                     \
      static constexpr int func (bool b)  \
       { return b ? printLocalVar : 0; }  \
    } ;                                   \
                                          \
   PrintStruct<local_foo>::func<true>(0); \
 }                                        \
()

Observe that the printed value, in the const version of PrintStruct::func(), is a template integer value; so can be used also for template arguments, C-style array dimensions, static_assert()s tests, etc.

Not sure that is perfectly standard (I'm not a really expert) and that do what you exactly want, but the following is a full working example

#include <iostream>

template <typename T>
struct PrintStruct
 {
   template <bool>
   static void func (...) 
    { std::cout << "func non-const: " << T::func(true) << std::endl; }

   template <bool b, int I = T::func(b)>
   static void func (int) 
    { std::cout << "func const:     " << I << std::endl; }
 };


#define Print(i)                          \
[&]()                                     \
 {                                        \
   static int const printLocalVar { i };  \
                                          \
   struct local_foo                       \
    {                                     \
      static constexpr int func (bool b)  \
       { return b ? printLocalVar : 0; }  \
    } ;                                   \
                                          \
   PrintStruct<local_foo>::func<true>(0); \
 }                                        \
()

int main()
 {
   constexpr int  i { 2 };
   int const      j { 3 };
   int            k { 4 };
   int const      l { k+1 };

   Print(1);    // print func const:     1
   Print(i);    // print func const:     2
   Print(j);    // print func const:     3
   Print(k);    // print func non-const: 4
   Print(l);    // print func non-const: 5
   Print(2+2);  // print func const:     4
 }
like image 135
max66 Avatar answered Oct 28 '22 11:10

max66


For detecting the constexpr suitability one may consider this GCC-only suggestion from @JohannesSchaub-litb (see linked answer for limitations):

template<typename T> 
constexpr typename remove_reference<T>::type makeprval(T && t) {
  return t;
}

A working example with different cases reads

#include <iostream>

////////////////////////////////////////////////////////////////////////////////

// https://stackoverflow.com/a/13305072/2615118

template<class T>
constexpr std::remove_reference_t<T> makeprval(T&& t) {
  return t;
}

#define isprvalconstexpr(e) noexcept(makeprval(e))

////////////////////////////////////////////////////////////////////////////////

template<bool is_constexpr, class Lambda>
struct HybridArg {
  using T = std::invoke_result_t<Lambda>;

  Lambda lambda_;
  constexpr operator T() const { return lambda_(); }// implicit conversion
};

template<bool is_constexpr, class Lambda>
constexpr auto make_hybrid_arg(Lambda lambda) {
  return HybridArg<is_constexpr, Lambda>{lambda};
}

#define WRAP_ARG(arg)                     \
  make_hybrid_arg<isprvalconstexpr(arg)>( \
    [&] { return arg; }                   \
  )                                       \

////////////////////////////////////////////////////////////////////////////////

template<int i>
void print_impl_constexpr() {
  std::cout << i << ": ";
  std::cout << __PRETTY_FUNCTION__ << std::endl;
}

void print_impl_fallback(int i) {
  std::cout << i << ": ";
  std::cout << __PRETTY_FUNCTION__ << std::endl;
}

////////////////////////////////////////////////////////////////////////////////

// option 1 (for choosing implementation):
// compile-time introspection

template<class Arg>
struct is_constexpr_arg : std::false_type {};

template<class Lambda>
struct is_constexpr_arg<
  HybridArg<true, Lambda>
> : std::true_type {};

template<class Arg>
void print_by_introspection(Arg arg) {
  if constexpr(is_constexpr_arg<Arg>{}) {
    print_impl_constexpr<arg>();
  }
  else {
    print_impl_fallback(arg);
  }
}

////////////////////////////////////////////////////////////////////////////////

// option 2 (for choosing implementation):
// overload

void print_by_overload(int arg) {
  print_impl_fallback(arg);
}

template<class Lambda>
void print_by_overload(HybridArg<true, Lambda> arg) {
  print_impl_constexpr<arg>();
}

////////////////////////////////////////////////////////////////////////////////

template<class Arg>
void indirection(Arg arg) {
  print_by_introspection(arg);
  print_by_overload(arg);
}

void bad_indirection(int arg) {
  print_by_introspection(arg);
  print_by_overload(arg);
}

////////////////////////////////////////////////////////////////////////////////

int main() {
  {
    int i = 0;
    indirection(i);
  }
  {
    int i = 1;
    indirection(WRAP_ARG(i));
  }
  {
    constexpr int i = 2;
    indirection(WRAP_ARG(i));
  }
  {
    constexpr int i = 3;
    bad_indirection(WRAP_ARG(i));
  }
}

Output after compilation with GCC:

0: void print_impl_fallback(int)
0: void print_impl_fallback(int)
1: void print_impl_fallback(int)
1: void print_impl_fallback(int)
2: void print_impl_constexpr() [with int i = 2]
2: void print_impl_constexpr() [with int i = 2]
3: void print_impl_fallback(int)
3: void print_impl_fallback(int)
like image 30
Julius Avatar answered Oct 28 '22 12:10

Julius