C++17 If statement with initializer
Introduced under proposal P00305r0, If statement with initializer
give us the ability to initialize a variable within an if
statement, and then, once initialized, perform the actual conditional check.
If statements
Pre C++17
When inserting into a map, if you want to check whether the element was inserted into the map or not, you need to perform the insertion, capture the return value, and then check for successful insertion
Example:
#include <iostream>
#include <map>
int main()
{
std::map<std::string, int> map;
map["hello"] = 1;
map["world"] = 2;
// first we have to perform the operation and capture the return value
auto ret = map.insert({ "hello", 3 });
// only then can we check the return value
if (!ret.second)
std::cout << "hello already exists with value " << ret.first->second << "\n";
// ret has "leaked" into this scope, so we need to pick a different name, ret2
auto ret2 = map.insert({ "foo", 4 });
// now we can check the value of ret2
if (!ret2.second)
std::cout << "foo already exists with value " << ret2.first->second << "\n";
return 0;
}
Build and run:
$ clang++ -std=c++14 main.cpp
$ ./a.out
hello already exists with value 1
There are 2 annoyances with this code (albeit fairly minor).
-
It is more verbose; we have to create the
ret
variable which we want to check for successful insertion, and then, only once the variable has been created, can we then perform the actual check. -
Slightly more insidious, the
ret
variable “leaks” into the surrounding scope. Note for the 2nd insertion we have to call the result variableret2
, becauseret
already exists.
It is quite easy to fix the 2nd annoyance by putting each conditional check into its own scope. This, however, comes at the cost of making the code even more verbose, and results in these floating braces with no preceding statement.
#include <iostream>
#include <map>
int main()
{
std::map<std::string, int> map;
map["hello"] = 1;
map["world"] = 2;
// we create a scope to enclose ret, preventing it leaking into the surrounding scope
{
auto ret = map.insert({ "hello", 3 });
if (!ret.second)
std::cout << "hello already exists with value " << ret.first->second << "\n";
}
// we create another scope to enclose ret, again preventing it from leaking out
{
auto ret = map.insert({ "foo", 4 });
if (!ret.second)
std::cout << "foo already exists with value " << ret.first->second << "\n";
}
return 0;
}
Build and run:
$ clang++ -std=c++14 main.cpp
$ ./a.out
hello already exists with value 1
C++17
With the introduction of if statement with initializer, we can now create the variable inside the if statement.
if (init; condition)
This makes the code more succint and doesn’t leak the variable into the surrounding scope.
Example:
#include <iostream>
#include <map>
int main()
{
std::map<std::string, int> map;
map["hello"] = 1;
map["world"] = 2;
// initialize the condition we want to check from within the if statement
if (auto ret = map.insert({ "hello", 3 }); !ret.second)
std::cout << "hello already exists with value " << ret.first->second << "\n";
// ret has not leaked, so we can use that for this conditional check too
if (auto ret = map.insert({ "foo", 4 }); !ret.second)
std::cout << "foo already exists with value " << ret.first->second << "\n";
return 0;
}
Build and run:
$ clang++-4.0 -std=c++1z main.cpp
$ ./a.out
hello already exists with value 1
std::unique_lock
Following comments on reddit by /u/holywhateverbatman, a perhaps more illustrative example would be the situation where you attempt to obtain a lock, but need to do something else if it is not currently available.
Here we attempt to lock a std::mutex
with a std::unique_lock
, specifying we should only try_to_lock
.
We can initialize the std::unique_lock
from within the if statement that then checks whether we were able to lock the mutex or not.
Example:
#include <iostream>
#include <mutex>
int main()
{
std::mutex mtx;
// create an RAII style lock guard, but don't block if we can't lock - check to
// see whether we were able to get the lock or not
if (std::unique_lock<std::mutex> l(mtx, std::try_to_lock); l.owns_lock())
{
std::cout << "successfully locked the resource\n";
//...
}
else
{
std::cout << "resource not currently available\n";
}
return 0;
}
Build and run:
$ clang++-4.0 -std=c++1z main.cpp
$ ./a.out
successfully locked the resource
A slightly more contrived example showing the mutex being unavailable:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
int main()
{
// a mutex which will protect a shared resource
std::mutex mtx;
// start a thread which obtains the mutex and then sleeps for 2 seconds
// (to give enough time for main thread to attempt to lock the mutex)
std::thread t([&]()
{
std::lock_guard<std::mutex> l(mtx);
std::this_thread::sleep_for(std::chrono::seconds(2));
});
// sleep for a short while to give thread t enough time to start and obtain the mutex
std::this_thread::sleep_for(std::chrono::seconds(1));
// when we try to lock in our if-with-initializer it should fail, as t owns the lock
if (std::unique_lock<std::mutex> l(mtx, std::try_to_lock); l.owns_lock())
{
std::cout << "successfully locked the resource\n";
//...
}
else
{
std::cout << "resource not currently available\n";
}
// wait for t to finish before exiting
if (t.joinable())
t.join();
return 0;
}
Build and run:
$ clang++-4.0 -std=c++1z main.cpp
$ ./a.out
resource not currently available
Combining with structured bindings
The usefulness of if with initializer becomes more apparent when combined with structured bindings.
In the following example we use structured bindings to unwrap the std::pair
return value of std::map::insert
into two
separate variables, it
(the iterator) and inserted
(a boolean indicating whether the insert succeeded).
We can then check whether the insert succeeded in the if statement by checking inserted
.
Example:
#include <iostream>
#include <map>
int main()
{
std::map<std::string, int> map;
map["hello"] = 1;
map["world"] = 2;
// intitialize the condition we want to check from within the if statement
if (auto [it, inserted] = map.insert({ "hello", 3 }); !inserted)
std::cout << "hello already exists with value " << it->second << "\n";
// ret has not leaked, so we can use that for this conditional check too
if (auto [it, inserted] = map.insert({ "foo", 4 }); !inserted)
std::cout << "foo already exists with value " << it->second << "\n";
return 0;
}
Build and run:
$ clang++-4.0 -std=c++1z main.cpp
$ ./a.out
hello already exists with value 1
Switch statements
Pre C++17
Similar annoyances exist with switch statements. The code is more verbose as the variable has to be initialized first, and then only can the switch occur. Similarly, the variable leaks into the surrounding scope.
Example:
#include <iostream>
enum Result
{
SUCCESS,
DEVICE_FULL,
ABORTED
};
std::pair<size_t /* bytes */, Result> writePacket()
{
return { 100, SUCCESS };
}
int main()
{
// initialize the value we want to switch on (res ends up in surrounding scope)
auto res = writePacket();
// then switch on that value
switch (res.second)
{
case SUCCESS:
std::cout << "successfully wrote " << res.first << " bytes\n";
break;
case DEVICE_FULL:
std::cout << "insufficient space on device\n";
break;
case ABORTED:
std::cout << "operation aborted before completion\n";
break;
}
return 0;
}
Build and run:
$ clang++ -std=c++14 main.cpp
$ ./a.out
successfully wrote 100 bytes
C++17
As with if statements, we can now create the variable inside the switch statement.
switch (init; var)
Example:
#include <iostream>
enum Result
{
SUCCESS,
DEVICE_FULL,
ABORTED
};
std::pair<size_t /* bytes */, Result> writePacket()
{
return { 100, SUCCESS };
}
int main()
{
switch (auto res = writePacket(); res.second)
{
case SUCCESS:
std::cout << "successfully wrote " << res.first << " bytes\n";
break;
case DEVICE_FULL:
std::cout << "insufficient space on device\n";
break;
case ABORTED:
std::cout << "operation aborted before completion\n";
break;
}
return 0;
}
Note now that we can initialize res
inside the switch
statement, and then perform the switch.
$ clang++-4.0 -std=c++1z main.cpp
$ ./a.out
successfully wrote 100 bytes