16

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.

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
  • 1
    I recall GCC having something like `__builtin_constant_p`. However, with a macro, I'm sure there are more portable ways, but that macro would have to be user-facing for sure. – chris Dec 06 '17 at 00:56
  • 2
    A constexpr can be detected as per here: https://stackoverflow.com/a/15236647/1294207 Maybe that can help? – Fantastic Mr Fox Dec 06 '17 at 00:58
  • By the way, you say "simply" overload with constexpr parameters, but it's really not. This has sparked huge discussions when being proposed for standardization. – chris Dec 06 '17 at 01:29
  • @chris - perhaps I used that word too loosely. I didn't mean to imply it was simple, but rather that if such functionality were available it would at least probably be simple for me to use it. I've edited that part to move "simply". – BeeOnRope Dec 06 '17 at 01:32
  • "In particular, in the "is constant" branch I should be able to treat i as a constexpr, including using it for template arguments, etc." - if `i` is a argument of the function, I don't think it's possible. – max66 Dec 06 '17 at 02:18
  • 1
    @max66 - right, but 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. I just mean the user of the code should write something like `print(5)` and internally I want to be able to take a `constexpr` code path or not depending on if the argument can be used as a constexpr. – BeeOnRope Dec 06 '17 at 02:26
  • 1
    You can use variations of the argument counting macro trick to extend a macro solution to multiple arguments (see: https://stackoverflow.com/q/11317474/315052), so you could define `Print1`, `Print2`, ... etc, and then `Print(...)` would invoke the right `PrintN` macro based on the result of the argument counting macro. – jxh Dec 06 '17 at 22:44
  • @chris - the `__builtin_constant_p` macro turns out not to be very useful for this case. It actually answers the question "has the optimizer determined this expression to have a constant value", which is much broader than true constexpr. For example something like `x - x` for variable `x` always returns true for this in GCC, but you can't treat it as a constant at the language level (eg use it for an array size, template argument, etc). – BeeOnRope Dec 06 '17 at 22:48

2 Answers2

4

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
 }
max66
  • 65,235
  • 10
  • 71
  • 111
  • Surround the statement block of the macro with `do` and `while(0)` so that the macro will expand into a statement. This avoids syntactical issues with `else`, such as `if (cond) Print(x); else { some; other; stuff; }` – jxh Dec 06 '17 at 17:36
  • 1
    @max66: That's what is done in C for macros. For C++, I guess you could try to convert your block into a lambda that is directly invoked, and this would get you an expression. – jxh Dec 06 '17 at 17:58
  • @jxh - I've worked a lot of years with C (a lot of years ago) but I don't remember this trick; about lambda... well, I don't see a way do do it; but maybe I'm wrong... today I don't have time but, maybe tomorrow, I'll try. If you have success converting my macro in a lambda, please, write it as an answer. I'm interested in it for other type of problems. – max66 Dec 06 '17 at 18:09
  • @max66: Oh, I meant the macro would expand into a lambda that is invoked. So, `do` becomes `[]()` and `while(0)` becomes `()`. – jxh Dec 06 '17 at 18:13
  • @jxh - Oh... in this sense... well, doesn't remove the original sin (the use of a macro) but, yes... it's more C++ (or less C) stylish; thanks again. – max66 Dec 06 '17 at 18:23
  • This is the kind of evil that I like :). I expected it to need a macro anyways, and I never really put macro-use in the same category as say child molestation or drowning kittens as some people seem to. I guess this doesn't work if you pass some expression as `i`, like `2 + 2` since then the `val ## i` won't produce a valid identifier. Why do you need to mangle the name of `val` like that anyways? Isn't it already unique within the scope? – BeeOnRope Dec 06 '17 at 20:05
  • 1
    @BeeOnRope - correct: doesn't work with expressions; about `val ## i` is to avoid that you call pass a variable to `Print` with the same name of the local static variable; but taking in count that this prevent to works with expressions, I suppose it's a bad idea and that it's better fix a long and strange name for the local variable. – max66 Dec 06 '17 at 20:16
  • 1
    @BeeOnRope - macro modified to accept expressions (but don't call it with an expression containing `printLocalVar` :( ) – max66 Dec 06 '17 at 20:27
  • You can unique-ify the local variable by adding `__` to its name someplace, or you can append the line number (which might need another macro to allow `__LINE__` to expand out to the number first before you apply `##` to paste it). – jxh Dec 06 '17 at 20:29
  • @jxh - yes, but... how this can avoid a name collision if the caller call `Print()` with the same name (example: if I give the name `foo__` to the internal static variable and the caller pass an external `foo__` ?) – max66 Dec 06 '17 at 20:43
  • It is just to make it more unlikely. Since you are already relying on `Print` and `PrintStruct` not really conflicting, you can use `PrintStruct__arg` for example. – jxh Dec 06 '17 at 20:51
  • To print out expressions: https://tio.run/##jZNNT8MwDIbv@RWvmIQaBoKNcelKOXNDghugqUsDhHVp1aVoqNpvH04KJHzsw4eosp/Yr@1UVNXJsxDrdU9pUTS5RKLKhallNk@Z95FH6eeUMSPnVZEZcpn3SupsLnGXMgo3wuCGIHPrvhlaBsDj07IsUutamMwogbdS5XhqtEC0MHkcdxWw5LAQWjivKBuDJMGBI3WpT0SpFybGgfUu7eE4qfNijBX7WxPTY5AqXOMSd3Fs80RTvkGJSw7xktU42qrkU4W1DUqwGjPGerl8Ulp2k4kUxzZ7YPeHjxFvsZ89BD3YDjvxrtJkUryRcEUifuN@UZOJkXSh3ZLdF3DJ5bKqXaluWt14ucvwhZPV0jS1xhRXgZgYZzSVn9kDcfu0GryuJGgg7ZaaUECmUU/xMeGriDNmlc4zpSPePcafPdB0Wgztmijk5@fslULnQSiwGYVG/98qKDTrD9zmv/RGA@66PD1FVX@PLng@A4@qHejQo6870HOPzv5Dgz9p5NFiB3rh0WF/aOGNAkZstV5/AA – jxh Dec 06 '17 at 20:54
2

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)
Julius
  • 1,816
  • 10
  • 14
  • Unless I'm missing something, this requires the caller to explicitly flag whether each argument is constant or not. If you are willing to impose that restriction it seems there are many possible solutions - for example, simply call `print<5>()` instead of `print(5)` for constant arguments (at least for types allowable in template type parameters). The idea (the hard part, I think) is to automatically detect the constness of the argument without requiring much cooperation from the caller. – BeeOnRope Dec 06 '17 at 18:38
  • @BeeOnRope: Yes, before my edit it was the responsibility of the caller to flag the argument type. See my updated answer if you are interested in a C++17 attempt with automatic flagging. – Julius Dec 06 '17 at 20:45
  • It seems impossible to make this work with Clang, though. – Julius Dec 06 '17 at 21:02