Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this usage of C++17 if constexpr fail?

I am trying to use C++17 if constexpr for conditional compilation, but it does not behave the way I expect.

For example, with the code below, C++ still compiles the code defined by the macro X2,

#include <map>
#include <string>
#include <iostream>
#include <type_traits>

#define X1 pp("x")
#define X2 pp("x", "x")

void pp(const std::string str)
{
   std::cout << str << std::endl;
}

int main()
{
   std::map<std::string, int> map;

   if constexpr (std::is_null_pointer_v<decltype(map)>)
      X2;
   else
      X1;
}

and spits out this error messages:

1.c:6:23: error: too many arguments to function ‘void pp(std::__cxx11::string)’
 #define X2 pp("x", "x")
                       ^
1.c:18:3: note: in expansion of macro ‘X2’
   X2;
   ^~

How can I skip compilation of X2?

like image 515
cow Avatar asked Jul 23 '20 03:07

cow


People also ask

What is the point of constexpr?

constexpr indicates that the value, or return value, is constant and, where possible, is computed at compile time. A constexpr integral value can be used wherever a const integer is required, such as in template arguments and array declarations.

Which is false about constexpr?

Short answer: static_assert(false) should never appear in a constexpr if expression, regardless of whether it's in a template function or whether it's in the discarded branch.


4 Answers

This is not possible outside the template!

From cppreference.com

Outside a template, a discarded statement is fully checked. if constexpr is not a substitute for the #if preprocessing directive:

void f() {
    if constexpr(false) {
        int i = 0;
        int *p = i; // Error even though in discarded statement
    }
}

Any idea how to skip compilation of X2?

One option is to provide a template function for that.
template<typename T>
void test()
{
   if constexpr (std::is_null_pointer_v<T>)
      X2;
   else
      X1;
}

int main()
{
   std::map<std::string, int> map;
   test<decltype(map)>();   // now chooses the X1
}

Thanks to @HolyBlackCat and @MSalters. As they pointed out, the above solution is ill-formed NDR (therefore, compiling with MSVC compiler does not make any sense and on the other hand the GCC and clang at least catch this by providing some compiler errors ) which has been detailed in the @HolyBlackCat's, answer!

Therefore can we skip the compilation of X2?

Unfortunately, NO as per your code!! The preprocessor will be executed before the compilation of the translation unit. Therefore one can not provide the type information (i.e. decltype(map)) to #if directives. Hence for your case, there is no other way.

Good lessons from this post:

  • Your program is, however, is a good example to avoid such kind macro and constexpr if mixing.
  • Secondly, check the correctness of the code by different compilers if possible!

I would suggest having a function overload for PP (and of course there are many other ways) to your case, by which you could get a well-formed code:

See a demo.

#include <string>
#include <iostream>
#include <type_traits>
#include <map>

void pp(const std::string& str)
{
   std::cout << str << std::endl;
}

template<typename... T> void pp(const T&... args)
{
   // do something with args!
}

template<typename T>
constexpr void test()
{
   if constexpr (std::is_null_pointer_v<T>)
      pp("x", "x"); // call with args
   else
      pp("x"); // call with string
}
like image 174
JeJo Avatar answered Oct 17 '22 11:10

JeJo


if constexpr is not really a "conditional compilation".

Outside of a template, it works just like the regular if (except it wants the condition to be constexpr).

The other answers suggest putting it inside of a template (and making the condition depend on the template parameter), but that's not enough. (It seems to work in MSVC, but not in GCC & Clang.) That's because:

[temp.res]/8.1 (emphasis mine)

The validity of a template may be checked prior to any instantiation. ... The program is ill-formed, no diagnostic required, if:

— no valid specialization can be generated for a template or a substatement of a constexpr if statement within a template and the template is not instantiated, ...

So if you can't make a valid instantiation for an if constexpr branch (that is, if for all possible template arguments the branch is invalid), then the program is ill-formed NDR (which effectively means "invalid, but the compiler might not be smart enough to give you an error").

(As noted by @MSalters, the standard says "and the template is not instantiated", rather than "and the template or the substatement of the constexpr if are not instantiated". I argue that it's a defective wording, because it makes no sense otherwise: there doesn't seem to be any other wording to check validity of discarded branches, so it would make the code well-formed only when the enclosing template is instantiated, and ill-formed NDR otherwise. See discussion in the comments.)

There seem to be no workarounds for that, and no good solutions for your problem.

You could make the function call itself depend on the template parameter, but it's probably cheating, as it requires shadowing pp (or doing #define pp …).

template <typename F>
void test(F pp) // Note parameter shadowing the global `pp` for the macros.
{
    std::map<std::string, int> map;

    if constexpr (std::is_null_pointer_v<decltype(map)>)
        X2;
    else
        X1;
}

int main()
{
    test([](auto &&... params)
    {
        pp(decltype(params)(params)...);
    });
}
like image 31
HolyBlackCat Avatar answered Oct 17 '22 11:10

HolyBlackCat


Outside of a template, even the false branch of an if constexpr is fully checked. In general, for this, one would need to

  • either use a #if pre-processor directive,
  • or put the if constexpr code into a template.

In your case, you can not use #if because your condition depends on type information that is not available to the pre-processor.

Also, you can not use constexpr if because the expansion of the macro X2 is always ill-formed, for any possible template parameter.

You probably need to rethink why you want to have a macro whose expansion is never valid.

like image 7
cigien Avatar answered Oct 17 '22 12:10

cigien


If possible, you can modify the pp to behave the way I think it should behave:

void pp(auto const &...args){
    ((std::cout << std::forward<decltype(args)>(args)), ...) << std::endl;
}

Link to compiler explorer

If that's not possible, then I suggest fixing the X2 to a code that compiles at least.

If changing X2 is not possible, then I suggest shadowing the pp for when calling x2 even though It's the best solution of course:

if constexpr (std::is_null_pointer_v<decltype(map)>) {
    auto pp = [] (auto const &...args){
        ((std::cout << std::forward<decltype(args)>(args)), ...) << std::endl;
    };
    X2;
} else {
    X1;
}

Link to compiler explorer

like image 3
moisrex Avatar answered Oct 17 '22 11:10

moisrex