Bezpečné programování v C++ I

9. 3. 2009 23:54 (aktualizováno) inkvizitor

Nedávno jsem měl debatu s Laelem Ophrirem, který tvrdil, že linuxové prostředí, kde se používají zejména jazyky C a C++, je z principu náchylnější k chybám v kódu, než prostředí Windows, kde se čím dál tím více prosazují „managed“ jazyky typu C#. Tenkrát jsem slíbil, že napíšu do blogu, proč to není pravda. V principu každý alespoň trochu rozumně napsaný program je možné pomocí statické analýzy vyšetřit a zjistit, zda je napsán korektně. V praxi to tak jednoduché není a netýká se to pouze jazyků typu C a C++, ale obecně jakéhokoliv programovacího jazyka. Při zachování určité disciplíny se lze ale kýženému stavu docela dobře přiblížit. V tomto miniseriálku se chci zaměřit na jazyk C++, který podle mého názoru umožňuje psát stejně bezpečné programy jako C# nebo Java.

Naše debata se týkala ověřitelnosti správnosti kódu napsaného v jednotlivých jazycích a Lael vyzdvihoval zejména nebezpečnost používání ukazatelů. Je třeba zdůraznit, že žádný program, který běží na současných mainstreamových počítačích, se neobejde bez použití ukazatelů. Na úrovni strojového kódu vždy dochází k explicitní alokaci a dealokaci paměti a k přístupu do této paměti pomocí ukazatelů. Na opačném konci stojí kód aplikačního programu, který může být napsaný v libovolném programovacím jazyce. Mezi těmito dvěma krajními póly stojí izolační vrstva, kterou může představovat interpret jazyka, překladač nebo knihovna. V zásadě nezáleží na tom, jaká izolační vrstva je v daném případě použita. Důležité je, že programátora odstíní od nízkoúrovňového kódu.

Když se omezíme na problematiku ukazatelů, lze jazyky rozdělit do tří kategorií: 1. Jazyky, které se bez použití ukazatelů dost dobře neobejdou – sem patří například C. 2. Jazyky, které ukazatele používají, ale z hlediska aplikačního programátora se bez nich lze docela dobře obejít – to je případ C++. 3. Jazyky, které ukazatele nepotřebují a ani v nich nejsou programátorovi ukazatele k dispozici. Sem patří široká škála jazyků funkcionálních, logických, jazyky typu C# a Java a spousta vyšších objektově orientovaných a hybridních programovacích jazyků. Zatímco u jazyků 3. kategorie je izolační vrstvou interpreter, kompilátor nebo jejich kombinace (např. překlad do interpretovaného bytecode), u C++ se musíme spokojit s knihovnami. To nám ale úplně stačí! U JVM, CLR apod. se rovněž musíme spokojit s tím, že důvěřujeme implementátorům prostředí, v němž programy běží.

Nezbedné ukazatele jsou ale jenom jedním z rizik, které v programovém kódu číhají. Jiné riziko představuje kód, který je plný funkcí (procedur) s vedlejšími efekty, které pořád dokola modifikují obsah proměnných nebo objektů. Kód, který omezuje takové praktiky na minimum a striktně odděluje „destruktivní“ akce od „nedestruktivních“, je z hlediska analýzy zcela jistě nejpřístupnější. Každou funkci, která pouze přijme na vstupu nějakou sadu hodnot a z nich vypočítá výsledek, lze zkoumat zcela odděleně. Místa, kde dochází k přepisování paměti, byť ne třeba pomocí ukazatelů, lze jasně označit a věnovat jim zvýšenou pozornost.

V C++ existuje spousta mechanismů, které umožňují vyhnout se použití ukazatelů a zvýšit bezpečnost kódu – reference na konstantní objekty, silná typová kontrola, mechanismus šablon a bohaté knihovny s kontejnerovými třídami – např. STL. Zároveň ale C++ umožňuje posílat ukazatele na funkce, takže v něm můžeme poměrně snadno implementovat principy známé z čistě funkcionálních jazyků.

V tomto dílu bych chtěl ukázat, jak je možné v C++ implementovat funkci filter() známou například z Pythonu.Tato funkce má dva argumenty – vstupní seznam a funkci, která rozhoduje, zda se hodnota ze vstupního seznamu objeví ve výstupním seznamu, který funkce filter() vrací. Funkce je velmi užitečná a pomáhá psát velmi elegantní a čitelné konstrukce. V Pythonu lze její použití demonstrovat následujícím prográmkem, který filtruje seznam celých čísel, tak, že v něm ponechá pouze sudá čísla:

#!/usr/bin/env python

l = [1, 2, 3]
print "Original list:", l
print "Filtered list:", filter(lambda x: ((x % 2) == 0), l)

Funkce filter() z jazyka Python ve verzi 3 chybí, ale lze ji snadno nahradit pomocí tzv. list comprehension:

#!/usr/bin/env python

l = [1, 2, 3]
print "Original list:", l
print "Filtered list:", [i for i in l if ((i % 2) == 0)]

Tato funkce má obdobu v různých jiných jazycích, byť se může jmenovat jinak nebo je nahrazena metodou dané třídy, která vrátí jiný objekt. Pro zajímavost přikládám prográmek v jazyce Scala:

#!/bin/sh
exec scala "$0" "$@"
!#

val l = List(1, 2, 3)

print("Original list: ")
println(l)

print("Filtered list: ")
println(l.filter(_ % 2 == 0))

Jak na to v C++? Třeba tak, že využijeme knihovny STL, která nabízí třídní šablonu s názvem list a definujeme si šablonu funkce, která umožní psát programy v podobném stylu, jaký nabízejí funkcionální jazyky. Náš prográmek bude vypadat následovně:

#include <list>
#include "safecode.hpp"
using namespace std;

bool isEven(const int &i)
{
    return ((i % 2) == 0);
}

int main(void)
{
    list<int> l;
    l.push_back(1);
    l.push_back(2);
    l.push_back(3);

    printList(l, "Original list");
    printList(filter(l, isEven), "Filtered list");

    return 0;
}

Program používá dvě šablony s názvem filter a printList. Jejich definice se nachází v souboru safecode.hpp, jehož obsah je následující:

#include <list>
#include <string>
#include <iostream>
using namespace std;

template<typename T>
void printList(const list<T> &l, const string &desc = "List contents")
{
    typename list<T>::const_iterator it;
    typename list<T>::size_type i = 0;

    cout << desc << ": [";

    for (it = l.begin(); it != l.end(); it++)
    {
        cout << *it;
        if (i < l.size() - 1)
            cout << ", ";

        i++;
    }

    cout << "]" << endl;
}

template<typename T>
list<T> filter(const list<T> &originalList, bool (* func)(const T &))
{
    list<T> newList;
    typename list<T>::const_iterator it;

    for (it = originalList.begin(); it != originalList.end(); it++)
    {
        if (func(*it))
            newList.push_back(*it);
    }

    return newList;
}

Všichni příznivci funkce filter() se mohou radovat – v C++ ji lze implementovat velmi snadno a používat ještě snadněji. Udělali jsme krůček k psaní snadno čitelného a srozumitelného kódu v C++. Příště bych chtěl uvést další příklady. Doufám, že si tento úvodní článek najde svoje příznivce nebo alespoň vyvolá zajímavou diskusi. Budu se těšit.

Update 9.3.2009: Na radu čtenáře s přezdívkou Sten jsem ve funkci printList() změnil typ proměnné i z int na list<T>::size_type. Děkuji.

Sdílet