trainings/AdvancedCppV2/Presentation/moder_cpp_cpp14.md

7.4 KiB

Cpp14

  • A quick reminder of lesser known features
    • decltype(auto)
    • Variable templates
    • Binary literals (Finaly!)
    • Digit separators

decltype

Rationale: Deduction provided in contexts where auto is not allowed.

decltype allows a compiler to deduce the type of the variable or expression, eg. the returned type can be deduced from function parameters.

std::map<std::string, float> collection;

decltype(collection) other;              // other has type of collection
decltype(collection)::mapped_type value; // value is float

template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) // from C++14 decltype not necessary
{
    return a + b;
}

It is useful in various scenarios:

auto compare = [](const auto &first, const auto &second) {
    if (first.size() == second.size()) {
    return first < second;
    }
    return first.size() < second.size();
};

std::map<std::string, int, decltype(compare)> map(compare);
map.emplace("C++20", 20);
map.emplace("C++1234", 1234);
map.emplace("Bababab", 12);
map.emplace("Abababa", 13);

for (const auto &[standard, number] : map) {
    std::cout << "Standard: " << standard << " | number: " << number << '\n';
    /* Output:
    Standard: C++20 | number: 20
    Standard: Abababa | number: 13
    Standard: Bababab | number: 12
    Standard: C++1234 | number: 1234 */
}

Since C++20 we can use lambda expressions in unevaluated operands:

using SquareRoot = decltype([](const int val) {
    return std::sqrt(val);
}); 

using Compare = decltype([](const auto &first, const auto &second) {
    if (first.size() == second.size()) {
        return first < second;
    }
    return first.size() < second.size();
});

int main() {
    std::vector<int> vec(30);
    std::iota(begin(vec), end(vec), 0);    
    std::transform(begin(vec), end(vec), begin(vec), SquareRoot{});
    for (const auto& el : vec) {
        std::cout << el << ' ';
    }
    // Output:
    // 0 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5

    // Compare lambda will be constructed by default
    std::map<std::string, int, Compare> map;
    map.emplace("C++20", 20);
    map.emplace("C++1234", 1234);
    map.emplace("Bababab", 12);
    map.emplace("Abababa", 13);
}

Closure types are not default constructible before C++20. In C++20 a closure type that has no capture is default constructible. That's why we can do a litle magic here :)


decltype(auto)

decltype(auto) deduction mechanism preserves type modifiers (references, const, volatile).

auto deduction mechanism does not preserve type modifiers.

When you write generic code you want to be able to perfectly forward a return type without knowing whether you are dealing with a reference or a value.

template<typename Fun, class... Args>
decltype(auto) Example(Fun fun, Args&&... args)
{
    return fun(std::forward<Args>(args)...);
}

Let's test it

We have the following class:

class Server {
public:
    bool addRequest(const std::string& serviceId, const std::string& request) {
        return requests_.emplace(serviceId, request).second;
    }

    std::string& getRequest(const std::string& serviceId) {
        if (const auto it = requests_.find(serviceId) ; it != std::cend(requests_)) {
            return it->second;
        }
        throw std::runtime_error("Invalid serviceId");
    }

private:
    std::map<std::string, std::string> requests_;
};

int main() {
    Server server;
    server.addRequest("SuperService", "Eat meat first!");

    server.getRequest("SuperService") += " Leave the potatoes";
    std::cout << server.getRequest("SuperService") << '\n';
    // Eat meat first! Leave the potatoes
}

Now let's use it with generic function, but without decltype(auto)

template<typename Fun, class... Args>
auto RunFun(Fun fun, Args&&... args)
{
    return fun(std::forward<Args>(args)...);
}

int main() {
    Server server;
    server.addRequest("SuperService", "Eat meat first!");

    // Write that we want to return std::string&
    RunFun([&server](const auto& id) -> std::string& { return server.getRequest(id); }, 
            "SuperService") += " Leave the potatoes";
    std::cout << server.getRequest("SuperService") << '\n';
    // Output: Eat meat first! 
    // Compiler didn't emit any warning!
}

Now fix this with decltype(auto)

template<typename Fun, class... Args>
decltype(auto) RunFun(Fun fun, Args&&... args)
{
    return fun(std::forward<Args>(args)...);
}

int main() {
    Server server;
    server.addRequest("SuperService", "Eat meat first!");

    // Write that we want to return std::string&
    RunFun([&server](const auto& id) -> std::string& { return server.getRequest(id); }, 
            "SuperService") += " Leave the potatoes";
    std::cout << server.getRequest("SuperService") << '\n';
    // Output: Eat meat first! Leave the potatoes
}

Now we avoid misleading and don't waste time on debugging sessions!


Variable templates

template <typename T>
constexpr T pi = T(3.141592653589793238462643383);

// Usual specialization rules apply:
template <>
constexpr const char* pi<const char*> = "pi";

template <>
constexpr const int pi<const int> = 4;

int main() {
    std::cout << pi<double> << '\n';       // 3.14159
    std::cout << pi<const char*> << '\n';  // pi
    std::cout << pi<int> << '\n';          // 3
    std::cout << pi<const int> << '\n';    // 4

    return 0;
}

Binary literals

int main() {
    std::cout << 0b10101010 << '\n';        // 170
    const int val = 0b1111;
    std::cout << (val ^ 0b1010) << '\n';    // 5

    return 0;
}

Digit separators

const int milion = 1'000'000;
const double val = 123'456'789'101.000;