diumenge, 2 de juliol del 2017

Aprendre a programar aux - C++11

Els llenguatges de programació evolucionen. Alguns més, i alguns menys. No obstant, els que no evolucionen estan condemnats a fracassar. I C++ és un llenguatge amb molts anys.

Una mica d'història

Als anys 70, Dennis Ritchie i Ken Thompson van voler portar Unix del PDP-7 al PDP-11, segons diu la llegenda, per jugar a un joc. No obstant, Unix estava programat en assemblador, i per tant el codi no era compatible. Aleshores, podrien haver arreglat el codi perquè funcionés en el PDP-11, però se'ls va acudir una cosa millor: Traduir el codi a un llenguatge que es pogués utilitzar en computadors amb assembladors diferents. 

Primer van pensar d'utilitzar el llenguatge B, que havia sigut dissenyat per Thompson i desenvolupat pels dos. No obstant, B no podia aprofitar diverses de les avantatges que tenia el PDP-11, tals com l'adreçament a nivell de byte (en B, es treballava sempre a nivell de word). Això els va portar a desenvolupar un nou llenguatge, conegut com a C (ja que és la lletra següent a la B). Estem a l'any 1972. Poc després van aparèixer les structs, fent que ja el llenguatge fos prou poderós com per programar gran part d'Unix en C.

Perquè us feu una idea, els sistemes operatius estan tots programats en C o C++. Els aparells empotrats (sí, es diuen així), també. Molts servidors també. C és el llenguatge dominant, està fins i tot a la sopa. Perquè us feu una idea, si estudieu a la FIB, només en els dos primers cursos utilitzareu C o C++ a PRO1, PRO2, EC, SO, CI, EDA, AC, IDI... No està malament, no?

C++

L'any 1980, algú pensa que a C li falta orientació a objectes. Així que es decideix afegir-li. Li toca a Bjarne Stroustrup. Inicialment se'l coneix com a C amb classes. No obstant, al 1983, Rick Mascitti, presumint d'originalitat informàtica decideix que, ja que és un increment de C, podem dir-li C++. C++ incorpora una sèrie de coses noves:

bool

Sí. En C, les comparacions retornaven un enter, que era 0 si era false, i diferent de 0 si era true. En C++, es crea el tipus bool, que només pot ser true o false (tot i que podem fer trampes per donar-li altres valors). A partir d'això, les comparacions retornen booleans, enlloc de retornar enters

wchar_t

En C els caràcters eren ASCII, i punt. Hi ha, com ja deveu saber, 128 caràcters ASCII. No obstant, amb tants idiomes com existeixen, necessitem molts més. Així apareix Unicode. No obstant, un caràcter Unicode acostuma a necessitar més d'un byte, mentre que en general, els chars ocupen un byte. Per això apareix wchar_t. En C ja existia, però no era un tipus, sinó que era 
typedef unsigned short wchar_t;
En C++ és un tipus. Té les seves pròpies funcions, classes i operadors, que tenen una w davant. Tenim wstring enlloc de string, wcout enlloc de cout...

main:

En C, el main teòricament havia de retornar un enter. No obstant, això no es complia, i vèiem coses com

int main()//La correcta
void main()
main()//Sí, sense tipus, pa qué
int main(int argc, char* argv[]);//També correcta
...
En C++ això s'acaba. El main té tres opcions, i si no és una d'aquestes, donarà error

int main();
int main (int argc, char* argv[]);
int main (int argc, char* argv[], char* env[]);

La primera és la que hem utilitzat sempre. La segona, rep paràmetres, concretament, un array de strings de C, i un enter amb la llargada d'argv. Aquest tindrà mida 1 com a mínim, perquè el primer paràmetre sempre és el nom de l'executable. 

Classes:

Bé, C++ és C amb classes, per tant no poden faltar les classes. Per saber què és una classe, hem de saber què és un objecte.
Un objecte és la representació informàtica d'alguna cosa. Normalment aquesta cosa existeix d'alguna manera. Per exemple, pot ser el cotxe del veí del 3r 1a
Una classe és l'abstracció d'un objecte. El cotxe del veí del 3r 1a, i el cotxe de la meva mare són cotxes els dos. Podem fer la classe cotxe, que tindrà atributs com les dimensions, el color, el model, el motor, les places... I, si volem representar el nostre cotxe, farem una instància d'aquesta classe amb les dades corresponents

Aleshores, una classe té una sèrie d'atributs, i una sèrie de funcions pròpies. Nosaltres la veiem com una capsa negra amb la que podem interactuar només amb una sèrie de funcions i operadors. No ens interessa saber com desem les dimensions, simplement que ens les doni si les necessitem. 

Un exemple és el vector. No necessitem saber com s'emmagatzema, simplement sabem que té l'operador [] per accedir als elements, que podem consultar-li la mida... Ens és igual que desi la mida (el que fa) o que la calculi (com a curiositat, la classe list, abans de C++11, calculava la mida cada cop que li demanàvem). Ens és igual que els elements s'emmagatzemin seguits (com ho fan), o que no...

Gràcies a les classes podem tenir vectors i strings. Els dos funcionen similar, un punter al primer element, un enter sense signe amb la mida, i una sèrie de funcions per interactuar.

Sobrecàrrega d'operadors

C++ permet sobrecarregar un operador. Això vol dir que, per exemple, podem crear l'operador < per una classe pròpia, i utilitzar-lo. Però no només amb classes, sinó també amb structs i enumeracions. 

C++11

Ara arriba la part important. C++11, també conegut com a C++0x (perquè havia de sortir abans del 2010, però no se sabia ben bé quan) és una gran actualització de C++, que li afegeix moltes funcionalitats noves. Algunes d'elles, molt i molt útils. 

Llistes d'inicialització:

En C, quan teníem un array, podíem fer una cosa així:

int arr[]={1,2,3,4,5};

Això amb els vectors no es podia fer. Al menys fins a C++11. Aquí apareixen llistes d'inicialització, que serveixen per tot tipus d'estructures de dades. Podem fer-ho amb vectors, amb structs, amb classes... Amb qualsevol cosa. Si, per exemple, volem fer un programa que faci els salts d'un cavall dels escacs, podem fer

const vector<pair<int,int>> SALTS={{1,2},{2,1},{-1,2}...};

S'amplia més a l'article sobre la STL

El puto espai dels templates

Suposo que algú s'haurà trobat que si fa
vector<vector<int>> Mat;
Li salta error. En canvi, si posem un espai entre els dos >, això ja no passa. C++11 ho soluciona, i ja no cal. Perquè realment era absurd

Inferència de tipus

Recordeu que, sempre que programem, hem de donar tipus a les variables? Hi ha vegades que és molt pesat. Per exemple, imaginem que volem utilitzar un iterador per recorrer el vector SALTS d'abans, que inicialment apunti al primer element. Hauríem de fer
vector<pair<int,int>>::iterator it=SALTS.begin();
No obstant, si la funció begin() retorna un iterador, no podríem deixar al compilador que decideixi de quin tipus serà it? La resposta és que sí, C++11 permet fer-ho

auto it=SALTS.begin();

No obstant, això només funciona en casos on estigui molt clar el tipus. El compilador tampoc és endeví. Si fem
auto x;
cin >>x
Se'ns queixarà, i amb raó

For en un rang

Quan volem recórrer tot un vector per fer alguna cosa, és un gran pal. Segur que algun cop heu desitjat que hi hagi una manera de recórrer tot un vector més fàcilment. Doncs C++11 porta aquesta manera

vector<int> v;
...
int parells=0;
for(int i:v) {
    if(i%2==0) ++parells;
}

Això agafarà tots els elements de v (li direm i a cada enter), i, per cada un, mirarà si és parell o no. També el podem utilitzar per llegir un vector

vector<int> v(n);
for (int& x:v) cin >>x;

Si posem &, la x serà una referència a l'element. Si no, serà una còpia, de manera que no la podrem modificar. 

vector<vector<int>> m(n,vector<int>(n));
for(auto& x:m) for(auto& y:x) cin >>y;

Utilitzant auto, aquests fors es fan molt més curts encara. En aquest cas, amb un codi molt curt llegim una matriu
Aquest for es pot fer amb qualsevol estructura de dades que tingui iteradors. És a dir, ho podem utilitzar amb vectors, llistes, sets, maps... Fins i tot amb arrays, que també tenen iteradors (amb la llibreria iterator, tenim les funcions begin(arr) i end(arr)).
El for en un rang equivaldrà a:
for(auto it=estructura.begin();it!=estructura.end()++it) {
    T x=*it;
    ...
}

Funcions i funcions lambda

Això és un gran canvi a C++. En C, utilitzar funcions com a paràmetre, era molt engorrós. Havies d'utilitzar punters. En C++ la cosa millora, perquè existeixen les referències. C++11 incorpora una classe anomenada function, que serveix per facilitar més encara això. La classe en qüestió la trobem a la llibreria functional, de la STL

Potser us pregunteu per què voldríem passar una funció com a paràmetre. Realment és una cosa que s'utilitza molt. Un exemple que nosaltres veiem és amb la funció sort, de la llibreria algorithm. Aquesta té dues versions. La primera rep dos iteradors (els direm inici i fi), i ordena en el rang [inici,fi) (és a dir, fi no inclòs), utilitzant l'operador <. Què passa si el que volem ordenar no té l'operador <? Què passa si volem ordenar amb un altre ordre? Doncs la segona versió accepta un tercer paràmetre, que serà, o bé una funció, o una classe, que servirà per comparar els elements. 

Una classe? Per què?

Utilitzar una classe té els seus avantatges. A la mateixa llibreria functional tenim definides les classes greater, greater_equal, less i less_equal. Aquestes classes utilitzen templates, de manera que tu, si vols ordenar un vector creixentment, simplement pots fer
sort(v.begin(),v.end(),greater<int>());
I no ens cal crear la funció per comparar. De la mateixa manera, podem fer greater_equal, less_equal... Això està millor explicat a l'article sobre la STL

Com utilitzem la classe function?

Fàcil. Imaginem que volem fer un mergesort, que ordeni un vector d'enters utilitzant el criteri que ens diuen. Necessitem que la funció rebi dos enters, i retorni un booleà. Fem això
void mergesort(vector<int>& v, function<bool(int,int)> cmp);
En els moments de la comparació, faríem
if(cmp(...))...
Tal qual. Ara, això és una millora, però no afegeix res que no poguéssim fer abans, simplement ens facilita la vida. El que de debò és una millora són les funcions lambda

Funcions lambda

Imaginem que volem fer un programa que llegeixi un vector, i un número, i depenent d'aquest número, ordena d'una manera o d'una altra. Aquí farem que si llegeix -1, ordena normal. Si llegeix -2, ordena en ordre invers. Si llegeix -3, ordena de manera que sigui creixent mirant els valors absoluts. Finalment, si llegeix -4, ordena com amb -3, però amb l'ordre invers. Un programa per fer-ho seria així:


bool cmp1(double a, double b) {
    return a>b;
}
bool cmp2(double a, double b) {
    return abs(a)<abs(b);
}
bool cmp3(double a, double b) {
    return cmp1(abs(a),abs(b));
}
int main() {
    int n;
    while(cin >>n) {
        vector<double>v(n);
        for(double& x:v) cin>>x;
        int ordre;
        cin>>ordre;
        switch(ordre) {
            case -1:
                sort(v.begin(),v.end());
                break;
            case -2:
                sort(v.begin(),v.end(),cmp1);
                break;
            case -3:
                sort(v.begin(),v.end(),cmp2);
                break;
            case -4:
                sort(v.begin(),v.end(),cmp3);
                break;
        }
        for(int i=0;i<n;++i) {
            cout<<(i?" ":"")<<v[i];
        }
        cout <<endl<<endl;
    }
}

Com es pot veure, tot el que utilitzo són coses que a C++98 ja existien. Però això és codi espagueti, fixem-nos que tenim allà unes funcions que ni cridem en el nostre codi, sinó que es criden des d'una altra funció. No necessitem tenir-les allà dalt, i de fet és contraproduent. Si un lector està llegint, quan arribi al cas -2, haurà de pujar fins a cmp1 per veure què fem. Amb el -3 igual, i amb el -4 igual. Imaginem que tinguéssim més casos així. Doncs encara més liat. 

I ens podem fer una pregunta: Igual que podem passar un valor directament, enlloc de passar una variable, per què no podem fer el mateix amb una funció? Per exemple, tu pots fer cout<<max(3,i). Doncs per què no hauria de poder passar la funció tal qual?

switch(ordre) {
case -1:
sort(v.begin(),v.end());
break;
case -2:
sort(v.begin(),v.end(),[](double a, double b){return a>b;});
break;
case -3:
  sort(v.begin(),v.end(),[](double a, double b){return abs(a)<abs(b);});
break;
case -4:
sort(v.begin(),v.end(),[](double a, double b){return abs(a)>abs(b);});
break;
}

Aquesta és la versió utilitzant funcions lambda. Anem per passos. 
Què és una funció lambda?
Una funció lambda és una funció anònima. Normalment no té nom, i acostuma a utilitzar-se per passar una funció com si fos un immediat
Com coi la declares?
Sí, una mica estrany. La versió més utilitzada és la que fa
[Captures](Paràmetres){Codi}
Què és això de la captura?
Una funció lambda pot capturar variables que estan al lloc des d'on la crides. Simplement li hem d'especificar quines, i si les captura per còpia o per referència
int foo(int a, int b) {
    int algo;
    cin>>algo;
    auto f=[algo](int a, int b) {return a+algo<b};
    int suma=0;
    for(int i=0;i<a;++i) {
        for(int j=0;j<b;++j) {
            if(f(i,j)) ++suma;
        }
    }
    return suma;
}
Aquí estem permetent que la funció lambda que he creat (aquí faig que f la cridi, perquè el codi queda més elegant) agafi la variable algo, i la utilitzi. 
Si vols que es capturi per referència, simplement faries
[&algo]...
Si vols establir que en general ho faci per referència, faries
[&,algo,algo2...]...
I agafaria per referència algo, algo2...
Si vols fer que en general ho faci per referència, però alguna variable concreta s'agafi per còpia, pots fer
[&,algo,...,=copia]...
També podem dir que per defecte ho faci per còpia, però ja és l'opció per defecte, i seria redundant. Com escriure unsigned int enlloc d'unsigned, o long long int enlloc de long long.
No li estàs dient què ha de retornar
No acostuma a caldre. En general, el compilador és prou intel·ligent com per saber-ho. Si la teva funció és prou complexa com perquè no ho sàpiga fer, ja se't queixarà. Aleshores caldrà utilitzar la forma
[Captures](Paràmetres) -> ret {Codi}
Aquí li dius què ha de retornar. Enlloc de ret, poses el tipus que retorna. És com molt python això
Aleshores, quan les utilitzo?
Una funció lambda té dos avantatges. El primer, que es pot escriure allà on la necessites. El segon, que pot consultar coses sense passar-li com a paràmetre. Per tant, jo les utilitzaria quan això et doni avantatge. Si necessites passar una funció com a paràmetre només en un moment, ho utilitzes. Si necessites que la funció accedeixi a diverses coses, però sense passar-li com a paràmetre

Usos des de la STL

La STL se'n beneficia molt, d'això, ja que hi ha una sèrie de funcions que reben una funció. A la llibreria algorithm podeu veure'n uns quants. Bàsicament hi ha moltes funcions que reben altres funcions, i justament allà les podem aprofitar. Si, per exemple, tenim un vector de strings, i volem ordenar-lo de manera que primer estiguin ordenats per longitud i, dins d'això, lexicogràficament, podríem fer

vector<string> v;
...
sort(v.begin(),v.end(),[](const string& s1, const string& s2) {
    if(s1.length()!=s2.length()) return s1.length()>s2.length();
    return s1<s2;
});
...

Cap comentari:

Publica un comentari a l'entrada