Sökresultat för

Använder du auto i C++?

6 minuter i lästid
Jens Riboe
Jens Riboe
Senior/Expert Software Developer
Använder du auto i C++?

En av mina favoriter i Modern C++, är användandet av auto. Det är när man deklarerar en variabel och låter kompilatorn klura ut dess typ baserat på typen för det uttryck som initierar denna. Detta ökar läsbarheten i koden, samt att man undviker att ha en oinitierad variabel. Det finns mer att säga om auto, och det är precis vad jag avser härnäst.

Låt oss börja med att titta på några variabler, deklarerade på klassiskt vis. Hur många sekunder tar det dig att identifiera samtliga variabler?

#include <string>
#include <vector>
#include <cstdint>
#include "model-types.hxx"

int v1 = 42;
unsigned long long v2 = 42;
unsigned long long* v3 = &b;
long double v4 = 3.1415;
std::string v5 = "Hi there";
ribomation::model::Person v6{"Anna Conda", 42};
ribomation::model::Account* v7 = new ribomation::model::Account{"ABC-123456", 150};
uint64_t v8 = 0xCAFEBABE;
std::vector<std::string> v9 = {"Hej", "Tjena", "Tjabba"};

Här följer nu samma variabler, men deklarerade på modernt vis. Hur många sekunder tar det dig nu att identifiera samtliga variabler?

#include <string>
#include <vector>
#include <cstdint>
#include "model-types.hxx"
using std::operator ""s;

auto v1 = 42;
auto v2 = 42ULL;
auto v3 = &b;
auto v4 = 3.1415L;
auto v5 = "Hi there"s;
auto v6 = ribomation::model::Person{"Anna Conda", 42};
auto v7 = new ribomation::model::Account{"ABC-123456", 150};
auto v8 = uint64_t{0xCAFEBABE};
auto v9 = std::vector{"Hej"s, "Tjena"s, "Tjabba"s};

Det här med läsbarhet av kod är en viktig aspekt, som gärna glöms bort. Vi läser programkod betydligt oftare än vi skriver densamma. Ta en titt på följande funktion, först skriven med west-return och sen med east-return.

#include <string>
#include <unordered_map>
#include <filesystem>
namespace fs = std::filesystem;

std::unordered_map<std::string, unsigned long> create_index(fs::path db_file);
#include <string>
#include <unordered_map>
#include <filesystem>
namespace fs = std::filesystem;

auto create_index(fs::path db_file) -> std::unordered_map<std::string, unsigned long>;

Enligt min uppfattning, så går det klart snabbare att identifiera de olika delarna av funktionen (namn, parametrar, returtyp) när den skrivs med s.k. east-return.

Apropå funktioner, fr.o.m. C++20 så kan man skriva parametrar med auto. Men redan i C++11, så kunde man skippa returtypen för enklare funktioner.

#include <iostream>
#include <string>
using std::operator ""s;
using std::cout;

auto operator *(auto s, auto n) -> std::string {
    return n > 0 ? s + (s * (n-1)) : s;
}

auto line(auto a, auto x, auto b) { return a * x + b; }

int main() {
    cout << "int   : " << line(2, 5, 10) << "\n";
    cout << "double: " << line(0.25, 3.5, 10.75) << "\n";
    cout << "mixed : " << line(2L, 3.5F, 10U) << "\n";
    cout << "string: " << line("!"s, 5U, " C++ is Cool"s) << "\n";
}
$ g++ -std-c++20 -Wall line.cxx -o line
$ ./line
int   : 20
double: 11.625
mixed : 17
string: !!!!!! C++ is Cool

Compiler Explorer Try it yourself on Compiler Explorer

Med C++11 kom for-each loopen (inspirerad av motsvarande i Java). Det formella namnet är förvisso range-based for-loop, men det orkar jag aldrig säga. Samt, i C++17 kom destructuring (structured binding), som innebär att man kan packa upp ett sammansatt primitivt objekt (inga constructors m.m.). Det här underlättar kolossalt, vid traversering av en std::map. Här nedan visat jag först det klassiska sättet och sedan det moderna dito.

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

int main () {
    std::map<std::string, unsigned long> word_freqs = {
        {"Hamlet", 113}, {"Juliet", 64}, {"Cleopatra", 111}
    };
    for (std::pair<std::string, unsigned long> p : word_freqs) 
        std::cout << p.first << ": " << p.second << "\n";
}
#include <iostream>
#include <string>
#include <map>

int main () {
    auto word_freqs = std::map<std::string, unsigned long>{
        {"Hamlet", 113}, {"Juliet", 64}, {"Cleopatra", 111}
    };
    for (auto [word, count] : word_freqs) 
        std::cout << word << ": " << count << "\n";
}
$ g++ -std-c++20 -Wall words.cxx -o words
$ ./words
Cleopatra: 111
Hamlet: 113
Juliet: 64

Compiler Explorer Try it yourself on Compiler Explorer

I version C++20 introducerades begreppet concept, som möjliggör att införa typ restriktioner på en template parameter. Detta kan kombineras med auto parametrar, vilket följande program illustrerar.

#include <concepts>
#include <numbers>
#include <string>
#include <print>

template<typename T>
concept numeric = ( std::integral<T> || std::floating_point<T> )
              && !( std::same_as<T, char> || std::same_as<T, bool> );

auto power(numeric auto x, std::integral auto n) {
    auto result = decltype(x){1};
    while (n-- > 0) result *= x;
    return result;
}

int main() {
    using std::numbers::pi;
    std::println("2^10   = {}", power(2, 10));
    std::println("pi^2   = {}", power(pi, 2));
    std::println("1.1^65 = {}", power(1.1, 'A'));
    
    //std::println("a^2 = {}", power('a', 2)); 
    //error: no matching function for call to 'power(char, int)'

    //std::println("2^2.5 = {}", power(2, 2.5));
    //error: no matching function for call to 'power(int, double)'

    //std::println("hej^2 = {}", power(std::string{"hej"}, 2));
    //error: no matching function for call to 'power(std::string, int)'
}

Jag börjar med att definiera typ restriktionen numeric, som antingen heltal eller flyttal. Eftersom char och bool räknas som integral values så undantar jag dessa.

Sedan kommer funktionen power, som tar ett numeriskt argument plus ett heltals argument. Variabeln result ges samma typ som parameter x och initieras med 1. I main visar jag några exempel på anrop, samt några vilka ger ett kompileringsfel.

$ g++ -std-c++20 -Wall power.cxx -o power
$ ./power
2^10   = 1024
pi^2   = 9.869604401089358
1.1^65 = 490.3707252978515

Compiler Explorer Try it yourself on Compiler Explorer

Det reserverade ordet auto är faktiskt ett av de äldsta keywords i C++ och går långt tillbaka till 1960-talet då språket B skapades.

compute(a, x, b) {
    auto result;
    result = a * x + b;
    return result;
}

Som synes, här ovan, så användes auto för att deklarera en lokal variabel. Språket B saknade typer och lokala variabler kallades på den tiden för automatic storage. När sedan språket C skapades i början på 1970-talet fanns det inget behov av auto, men man behöll det för att vara bakåt-kompatibel med B, eftersom man hade många program fragment som behövde länkas med C program. När sedan C++ skapades i slutet på 1970-talet så följde auto med av bara farten, eftersom C++ bygger på C, som ju byggde på B. Nåväl, efter åtskilliga decennier senare, kunde man omdefiniera auto till dess moderna innebörd.