DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report

Simplify Code With 'if constexpr' in C++17

Writing shorter, DRYer code is always good. In this post, we'll learn from an expert developer how to do this with C++ 17.

Bartłomiej Filipek user avatar by
Bartłomiej Filipek
·
Aug. 24, 18 · Tutorial
Like (1)
Save
Tweet
Share
8.28K Views

Join the DZone community and get the full member experience.

Join For Free

Before C++17, we had a few, quite ugly looking, ways to write static if ( if that works at compile time) in C++: you could use tag dispatching or SFINAE (for example via std::enable_if). Fortunately, that's changed, and we can now take benefit of if constexpr!

Let's see how we can use it and replace some std::enable_if code.

Intro

Static if in the form of if constexpr is an amazing feature that went into C++17. Recently @Meeting C++ there was a post where Jens showed how he simplified one code sample using if constexpr: How if constexpr simplifies your code in C++17.

I've found two additional examples that can illustrate how this new feature works:

  • Number comparisons.
  • Factories with a variable number of arguments.

I think those examples might help you to understand the static if from C++17.

But, for a start, I'd like to recall the basic knowledge about enable_if to set some background.

Why Compile Time if?

At first, you may ask, why do we need static if and those complex templated expressions... wouldn't normal if just work?

Here's some test code:

template <typename T>
std::string str(T t)
{
    if (std::is_same_v<T, std::string>) // or is_convertible...
        return t;
    else
        return std::to_string(t);
}

The above routine might be some simple utility that is used to print stuff. As to_string doesn't accept std::string we can test and just return t if it's already a string. Sounds simple... but try to compile this code:

// code that calls our function
auto t = str("10"s);

You might get something like this:

In instantiation of 'std::__cxx11::string str(T) [with T = 
std::__cxx11::basic_string<char>; std::__cxx11::string =
 std::__cxx11::basic_string<char>]':
required from here
error: no matching function for call to 
'to_string(std::__cxx11::basic_string<char>&)'
    return std::to_string(t);

is_same yields true for the type we used (string) and we can just return t, without any conversion... so what's wrong?

Here's the main point:

The compiler compiled both branches and found an error in the else case. It couldn't reject "invalid" code for this particular template instantiation.

So that's why we need a static if that will "discard" code and compile only the matching statement.

std::enable_if

One way to write a static if in C++11/14 is to use enable_if.

enable_if (and enable_if_v since C++14). It has quite strange syntax:

template< bool B, class T = void >  
struct enable_if;

enable_if will evaluate to T if the input condition B is true. Otherwise, it's SFINAE, and a particular function overload is removed from the overload set.

We can rewrite our basic example to:

template <typename T>
std::enable_if_t<std::is_same_v<T, std::string>, std::string> strOld(T t)
{
    return t;
}

template <typename T>
std::enable_if_t<!std::is_same_v<T, std::string>, std::string> strOld(T t)
{
    return std::to_string(t);
}

Not easy... right?

See below how we can simplify such code with if constexpr from C++17. After you read the post, you'll be able to rewrite our str utility quickly.

Use Case 1: Comparing Numbers

Let's start with a simple example: a close_enough function that works on two numbers. If the numbers are not floating points (like when we have two ints), then we can just compare it. Otherwise, for floating points, it's better to use some epsilon.

I've found this sample from at Practical Modern C++ Teaser - a fantastic walkthrough of modern C++ features by Patrice Roy. He was also very kind and allowed me to include this example.

C++11/14 version:

template <class T>
constexpr T absolute(T arg) {
   return arg < 0 ? -arg : arg;
}

template <class T>
constexpr enable_if_t<is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
   return absolute(a - b) < static_cast<T>(0.000001);
}
template <class T>
constexpr enable_if_t<!is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
   return a == b;
}

As you see, there's a use for enable_if. It's very similar to our str function. The code tests if the type of input numbers is is_floating_point. Then, the compiler can remove one function from the overload resolution set.

And now, let's look at the C++17 version:

template <class T>
constexpr T absolute(T arg) {
   return arg < 0 ? -arg : arg;
}

template <class T>
constexpr auto precision_threshold = T(0.000001);

template <class T>
constexpr bool close_enough(T a, T b) {
   if constexpr (is_floating_point_v<T>) // << !!
      return absolute(a - b) < precision_threshold<T>;
   else
      return a == b;
}

Wow... so just one function, that looks almost like a normal function. With a nearly "normal" if.

if constexpr evaluates constexpr expression at compile time and then discards the code in one of the branches.

By the way: Can you see some other C++17 features that were used here?

Use Case 2: Factory With Variable Arguments

In the item 18 of Effective Modern C++ Scott Meyers described a method called makeInvestment:

template<typename... Ts> 
std::unique_ptr<Investment> 
makeInvestment(Ts&&... params);

There's a factory method that creates derived classes of Investment and the main advantage is that it supports a variable number of arguments!

For example, here are the proposed types:

class Investment
{
public:
    virtual ~Investment() { }

    virtual void calcRisk() = 0;
};

class Stock : public Investment
{
public:
    explicit Stock(const std::string&) { }

    void calcRisk() override { }
};

class Bond : public Investment
{
public:
    explicit Bond(const std::string&, const std::string&, int) { }

    void calcRisk() override { }
};

class RealEstate : public Investment
{
public:
    explicit RealEstate(const std::string&, double, int) { }

    void calcRisk() override { }
};

The code from the book was too idealistic, and didn't work - it worked until all your classes have the same number and types of input parameters:

Scott Meyers: Modification History and Errata List for Effective Modern C++:

The makeInvestment interface is unrealistic, because it implies that all derived object types can be created from the same types of arguments. This is especially apparent in the sample implementation code, where are arguments are perfect-forwarded to all derived class constructors.

For example if you had a constructor that needed two arguments and one constructor with three arguments, then the code might not compile:

// pseudo code:
Bond(int, int, int) { }
Stock(double, double) { }
make(args...)
{
  if (bond)
     new Bond(args...);
  else if (stock)
     new Stock(args...)
}

Now, if you write make(bond, 1, 2, 3) - then the else statement won't compile - as there are no Stock(1, 2, 3) available! To work, we need something like static if - if that will work at compile time, and will reject parts of the code that don't match a condition.

Some posts ago, with the help of one reader, we came up with a working solution (you can read more in Bartek's coding blog: Nice C++ Factory Implementation 2).

Here's the code that could work:

template <typename... Ts> 
unique_ptr<Investment> 
makeInvestment(const string &name, Ts&&... params)
{
    unique_ptr<Investment> pInv;

    if (name == "Stock")
        pInv = constructArgs<Stock, Ts...>(forward<Ts>(params)...);
    else if (name == "Bond")
        pInv = constructArgs<Bond, Ts...>(forward<Ts>(params)...);
    else if (name == "RealEstate")
        pInv = constructArgs<RealEstate, Ts...>(forward<Ts>(params)...);

    // call additional methods to init pInv...

    return pInv;
}

As you can see the "magic" happens inside constructArgs function.

The main idea is to return unique_ptr<Type> when Type is constructible from a given set of attributes and nullptr when it's not.

Before C++17

In my previous solution (pre C++17) we used std::enable_if and it looked like that:

// before C++17
template <typename Concrete, typename... Ts>
enable_if_t<is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete>>
constructArgsOld(Ts&&... params)
{
    return std::make_unique<Concrete>(forward<Ts>(params)...);
}

template <typename Concrete, typename... Ts>
enable_if_t<!is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete> >
constructArgsOld(...)
{
    return nullptr;
}

std::is_constructible @cppreference.com - allows us to quickly test if a list of arguments could be used to create a given type.

In C++17 there's a helper:

is_constructible_v = is_constructible<T, Args...>::value;

So we could make the code shorter a bit...

Still, using enable_if looks ugly and complicated. How about C++17 version?

With if constexpr

Here's the updated version:

template <typename Concrete, typename... Ts>
unique_ptr<Concrete> constructArgs(Ts&&... params)
{  
  if constexpr (is_constructible_v<Concrete, Ts...>)
      return make_unique<Concrete>(forward<Ts>(params)...);
   else
       return nullptr;
}

We can even extend it with a little logging features, using fold expression:

template <typename Concrete, typename... Ts>
std::unique_ptr<Concrete> constructArgs(Ts&&... params)
{ 
    cout << __func__ << ": ";
    // fold expression:
    ((cout << params << ", "), ...);
    cout << "\n";

    if constexpr (std::is_constructible_v<Concrete, Ts...>)
        return make_unique<Concrete>(forward<Ts>(params)...);
    else
       return nullptr;
}

Cool... right?

All the complicated syntax of enable_if went away; we don't even need a function overload for the else case. We can now wrap expressive code in just one function.

if constexpr evaluates the condition and only one block will be compiled. In our case, if a type is constructible from a given set of attributes, then we'll compile make_unique call. If not, then nullptr is returned (and make_unique is not even compiled).

Wrapping Up

Compile-time if is an amazing feature that greatly simplifies templated code. What's more, it's much more expressive and nicer than previous solutions: tag dispatching or enable_if (SFINAE). Now, you can easily express yours intends similarly to "run-time" code.

In this article, we've touched only on basic expressions, and, as always, I encourage you to play more with this new feature and explore.

And going back to our str example, can you now rewrite str function using if constexpr?

More from the Author:

Bartek recently published a book - "C++17 In Detail"- rather than reading the papers and C++ specification drafts, you can use this book to learn the new Standard in an efficient and practical way.

c++

Published at DZone with permission of Bartłomiej Filipek, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • 10 Things to Know When Using SHACL With GraphDB
  • 5 Steps for Getting Started in Deep Learning
  • Journey to Event Driven, Part 1: Why Event-First Programming Changes Everything
  • Building a Real-Time App With Spring Boot, Cassandra, Pulsar, React, and Hilla

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: