TransWikia.com

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

Stack Overflow Asked by cow on December 5, 2021

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?

4 Answers

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

Answered by moisrex on December 5, 2021

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)...);
    });
}

Answered by HolyBlackCat on December 5, 2021

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
}

Answered by JeJo on December 5, 2021

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.

Answered by cigien on December 5, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP