Concaténation de deux vecteurs std ::


687

Comment concaténer deux std::vectors?


5
Les réponses données ne concaténent pas réellement. Ils en joignent une copie. Il peut être utile (pour le point de vue de l'efficacité) de créer une méthode de concaténation std :: vector, mais cela nécessiterait un partage sophistiqué de la gestion des nœuds et c'est probablement pourquoi cela n'a pas été fait.
FauChristian

8
@FauChristian: Non, il peut ne pas y avoir d'utilisation du point de vue de l'efficacité. La mémoire vectorielle doit être continue, donc ce que l'on vous suggère est impossible. Si vous vouliez "un partage sophistiqué de la gestion des nœuds", et si vous deviez changer la classe vectorielle de cette manière, vous vous retrouveriez avec un deque. Même alors, il est très difficile de réutiliser la mémoire de la manière suggérée, même si cela commencerait à être un peu plus réalisable. Je ne pense pas qu'il soit actuellement mis en œuvre. L'essentiel est que dans un tel partage de nœuds de gestion (un deque) le nœud final puisse être partiellement vide.
Cookie

4
@lecaruyer Vous vous rendez compte que vous venez de marquer une question qui a été posée deux ans auparavant en double
eshirima

9
Suis-je le seul à me demander pourquoi cela n'est pas implémenté comme a + bou a.concat(b)dans la bibliothèque standard? Peut-être que l'implémentation par défaut serait sous-optimale, mais chaque concaténation de tableau n'a pas besoin d'être micro-optimisée
oseiskar

10
des années d'évolution, la surcharge d'opérateur la plus avancée de tous les langages traditionnels, un système de modèles qui double la complexité du langage, et pourtant la réponse n'est pas v = v1 + v2;
Spike0xff

Réponses:


728
vector1.insert( vector1.end(), vector2.begin(), vector2.end() );

53
Je voudrais seulement ajouter du code pour obtenir d'abord le nombre d'éléments que chaque vecteur contient, et définir vector1 pour être celui qui contient le plus grand. Si vous faites autrement, vous faites beaucoup de copies inutiles.
Joe Pineda

34
J'ai une question. Est-ce que cela fonctionnera si vector1 et vector2 sont les mêmes vecteurs?
Alexander Rafferty

6
Si vous avez concaténé plusieurs vecteurs en un, est-il utile d'appeler reserved'abord le vecteur de destination?
Faheem Mitha

33
@AlexanderRafferty: Seulement si vector1.capacity() >= 2 * vector1.size(). Ce qui est atypique sauf si vous avez appelé std::vector::reserve(). Sinon, le vecteur se réallouera, invalidant les itérateurs passés en paramètres 2 et 3.
Drew Dormann

28
C'est dommage qu'il n'y ait pas d'expression plus succincte dans la bibliothèque standard. .concatou +=ou quelque chose
nmr

193

Si vous utilisez C ++ 11 et souhaitez déplacer les éléments plutôt que de simplement les copier, vous pouvez utiliser std::move_iteratoravec insert (ou copy):

#include <vector>
#include <iostream>
#include <iterator>

int main(int argc, char** argv) {
  std::vector<int> dest{1,2,3,4,5};
  std::vector<int> src{6,7,8,9,10};

  // Move elements from src to dest.
  // src is left in undefined but safe-to-destruct state.
  dest.insert(
      dest.end(),
      std::make_move_iterator(src.begin()),
      std::make_move_iterator(src.end())
    );

  // Print out concatenated vector.
  std::copy(
      dest.begin(),
      dest.end(),
      std::ostream_iterator<int>(std::cout, "\n")
    );

  return 0;
}

Cela ne sera pas plus efficace pour l'exemple avec des entiers, car les déplacer n'est pas plus efficace que les copier, mais pour une structure de données avec des mouvements optimisés, cela peut éviter de copier un état inutile:

#include <vector>
#include <iostream>
#include <iterator>

int main(int argc, char** argv) {
  std::vector<std::vector<int>> dest{{1,2,3,4,5}, {3,4}};
  std::vector<std::vector<int>> src{{6,7,8,9,10}};

  // Move elements from src to dest.
  // src is left in undefined but safe-to-destruct state.
  dest.insert(
      dest.end(),
      std::make_move_iterator(src.begin()),
      std::make_move_iterator(src.end())
    );

  return 0;
}

Après le déplacement, l'élément de src est laissé dans un état indéfini mais sûr à détruire, et ses anciens éléments ont été transférés directement vers le nouvel élément de dest à la fin.


6
La méthode std :: make_move_iterator () m'a aidé lors de la tentative de concaténation des vecteurs std :: de std :: unique_ptr.
Knitschi

Quelle est la différence entre cela et std::move(src.begin(), src.end(), back_inserter(dest))?
kshenoy


77

Ou vous pouvez utiliser:

std::copy(source.begin(), source.end(), std::back_inserter(destination));

Ce modèle est utile si les deux vecteurs ne contiennent pas exactement le même type de chose, car vous pouvez utiliser quelque chose au lieu de std :: back_inserter pour convertir d'un type à l'autre.


7
la méthode de copie n'est pas un si bon moyen. Il appellera push_back plusieurs fois, ce qui signifie que si de nombreux éléments doivent être insérés, cela pourrait signifier plusieurs réallocations. il est préférable d'utiliser l'insertion car l'implémentation vectorielle pourrait faire une certaine optimisation pour éviter les réallocations. il pourrait réserver de la mémoire avant de commencer la copie
Yogesh Arora

7
@Yogesh: d'accord, mais rien ne vous empêche d'appeler en reservepremier. La raison std::copyest parfois utile si vous souhaitez utiliser autre chose que back_inserter.
Roger Lipscombe

Lorsque vous dites "allocations multiples", c'est vrai - mais le nombre d'allocations est au pire journal (nombre d'entrées ajoutées) - ce qui signifie que le coût de l'ajout d'une entrée est constant dans le nombre d'entrées ajoutées. (Fondamentalement, ne vous en faites pas, sauf si le profilage montre que vous avez besoin d'une réserve).
Martin Bonner soutient Monica

Vous voudrez peut-être utiliser std :: transform pour le faire à la place.
Martin Broadhurst

1
copie suce beaucoup, même avec réserve. vector :: insert évitera toutes les vérifications: quick-bench.com/bLJO4OfkAzMcWia7Pa80ynwmAIA
Denis Yaroshevskiy

63

Avec C ++ 11, je préférerais suivre pour ajouter le vecteur b à a:

std::move(b.begin(), b.end(), std::back_inserter(a));

quand aet bne se chevauchent pas, et bne sera plus utilisé.


C'est std::movede <algorithm>, pas l' habituel std::move de <utility>.


10
Comportement indéfini si a est en fait b (ce qui est OK si vous savez que cela ne peut jamais arriver - mais qu'il vaut la peine d'être conscient du code à usage général).
Martin Bonner soutient Monica

1
@MartinBonner Merci d'avoir mentionné cela. Je devrais probablement revenir à l'ancienne insert, qui est plus sûre.
Deqing

15
Ah, l'AUTRE std :: move. Assez déroutant la première fois que vous le voyez.
xaxxon

1
Est - ce différent de insert()avec move_iterators? Si c'est le cas, comment?
GPhilo

1
J'ai ajouté une note sur ce dont std::movenous parlons ici, car la plupart des gens ne connaissent pas cette surcharge. J'espère que c'est une amélioration.
YSC

38
std::vector<int> first;
std::vector<int> second;

first.insert(first.end(), second.begin(), second.end());

24

Je préfère celui qui est déjà mentionné:

a.insert(a.end(), b.begin(), b.end());

Mais si vous utilisez C ++ 11, il existe un autre moyen générique:

a.insert(std::end(a), std::begin(b), std::end(b));

De plus, ne fait pas partie d'une question, mais il est conseillé de l'utiliser reserveavant de l'ajouter pour de meilleures performances. Et si vous concaténez le vecteur avec lui-même, sans le réserver, il échoue, alors vous devriez toujours le faire reserve.


Donc, fondamentalement, ce dont vous avez besoin:

template <typename T>
void Append(std::vector<T>& a, const std::vector<T>& b)
{
    a.reserve(a.size() + b.size());
    a.insert(a.end(), b.begin(), b.end());
}

2
std::est déduit par la recherche dépendante de l'argument . end(a)sera suffisant.
Asu

4
@Asu ADL n'ajoutera que std::si le type de aprovient std, ce qui vainc l'aspect générique.
Potatoswatter

bon point. dans ce cas, c'est un vecteur, cela fonctionnerait de toute façon, mais oui, c'est une meilleure solution.
Asu

std :: begin () / end () ont été ajoutés pour les collections (comme les tableaux) qui ne les ont pas comme fonctions membres. Mais les tableaux n'ont pas non plus de fonction membre insert () et appellent la question "Existe-t-il une collection avec un insert () mais sans begin () (qui fonctionne avec le std :: begin ())?"
James Curran


12

Vous devez utiliser vector :: insert

v1.insert(v1.end(), v2.begin(), v2.end());

3
N'est-ce pas la même chose avec la réponse donnée par Tom Ritter et Robert Gamble en 2008?
IgNite

9

Une amélioration générale des performances pour concaténer consiste à vérifier la taille des vecteurs. Et fusionnez / insérez le plus petit avec le plus grand.

//vector<int> v1,v2;
if(v1.size()>v2.size()) {
    v1.insert(v1.end(),v2.begin(),v2.end());
} else {
    v2.insert(v2.end(),v1.begin(),v1.end());
}

Si simple, pourtant je n'y ai jamais pensé de cette façon!
Zimano

2
L'exemple de code est incorrect. v1.insert(v2.end()...utilise un itérateur dans v2pour spécifier la position dans v1.
David Stone

Vous pouvez également utiliser un échange rapide. @DavidStone Je l'ai édité afin que l'ordre de concaturation puisse être changé. Est-il possible d'ajouter au début d'un vecteur?
qwr

Vous pouvez insérer au début, mais ce sera plus lent. Pour vraiment "concaténer", cependant, l'ordre importe généralement, c'est donc ce que vous devez faire.
David Stone

7

Si vous voulez pouvoir concaténer des vecteurs de manière concise, vous pouvez surcharger l' +=opérateur.

template <typename T>
std::vector<T>& operator +=(std::vector<T>& vector1, const std::vector<T>& vector2) {
    vector1.insert(vector1.end(), vector2.begin(), vector2.end());
    return vector1;
}

Ensuite, vous pouvez l'appeler comme ceci:

vector1 += vector2;

6

Si vous êtes intéressé par une garantie d'exception forte (lorsque le constructeur de copie peut lever une exception):

template<typename T>
inline void append_copy(std::vector<T>& v1, const std::vector<T>& v2)
{
    const auto orig_v1_size = v1.size();
    v1.reserve(orig_v1_size + v2.size());
    try
    {
        v1.insert(v1.end(), v2.begin(), v2.end());
    }
    catch(...)
    {
        v1.erase(v1.begin() + orig_v1_size, v1.end());
        throw;
    }
}

Similaire append_moveavec une forte garantie ne peut pas être implémenté en général si le constructeur de mouvement de l'élément vectoriel peut lancer (ce qui est peu probable mais quand même).


N'est-il pas possible v1.erase(...de lancer aussi?
Classe Skeleton du

insertgère déjà cela. De plus, cet appel à eraseest équivalent à a resize.
Potatoswatter

J'aime ça - merci!
natersoz

5

Ajoutez celui-ci à votre fichier d'en-tête:

template <typename T> vector<T> concat(vector<T> &a, vector<T> &b) {
    vector<T> ret = vector<T>();
    copy(a.begin(), a.end(), back_inserter(ret));
    copy(b.begin(), b.end(), back_inserter(ret));
    return ret;
}

et utilisez-le de cette façon:

vector<int> a = vector<int>();
vector<int> b = vector<int>();

a.push_back(1);
a.push_back(2);
b.push_back(62);

vector<int> r = concat(a, b);

r contiendra [1,2,62]


Je ne sais pas pourquoi cela a été rejeté. Ce n'est peut-être pas le moyen le plus efficace de le faire, mais ce n'est pas faux et c'est efficace.
leeor_net

4

Voici une solution à usage général utilisant la sémantique de déplacement C ++ 11:

template <typename T>
std::vector<T> concat(const std::vector<T>& lhs, const std::vector<T>& rhs)
{
    if (lhs.empty()) return rhs;
    if (rhs.empty()) return lhs;
    std::vector<T> result {};
    result.reserve(lhs.size() + rhs.size());
    result.insert(result.cend(), lhs.cbegin(), lhs.cend());
    result.insert(result.cend(), rhs.cbegin(), rhs.cend());
    return result;
}

template <typename T>
std::vector<T> concat(std::vector<T>&& lhs, const std::vector<T>& rhs)
{
    lhs.insert(lhs.cend(), rhs.cbegin(), rhs.cend());
    return std::move(lhs);
}

template <typename T>
std::vector<T> concat(const std::vector<T>& lhs, std::vector<T>&& rhs)
{
    rhs.insert(rhs.cbegin(), lhs.cbegin(), lhs.cend());
    return std::move(rhs);
}

template <typename T>
std::vector<T> concat(std::vector<T>&& lhs, std::vector<T>&& rhs)
{
    if (lhs.empty()) return std::move(rhs);
    lhs.insert(lhs.cend(), std::make_move_iterator(rhs.begin()), std::make_move_iterator(rhs.end()));
    return std::move(lhs);
}

Notez comment cela diffère de appending à a vector.


4

Vous pouvez préparer votre propre modèle pour l'opérateur +:

template <typename T> 
inline T operator+(const T & a, const T & b)
{
    T res = a;
    res.insert(res.end(), b.begin(), b.end());
    return res;
}

La prochaine chose - utilisez simplement +:

vector<int> a{1, 2, 3, 4};
vector<int> b{5, 6, 7, 8};
for (auto x: a + b)
    cout << x << " ";
cout << endl;

Cet exemple donne une sortie:

1 2 3 4 5 6 7 8

3
L'utilisation T operator+(const T & a, const T & b)est dangereuse, il vaut mieux l'utiliser vector<T> operator+(const vector<T> & a, const vector<T> & b).
Matthieu H

4

Il y a un algorithme std::mergede C ++ 17 , qui est très facile à utiliser,

Voici l'exemple:

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    //DATA
    std::vector<int> v1{2,4,6,8};
    std::vector<int> v2{12,14,16,18};

    //MERGE
    std::vector<int> dst;
    std::merge(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(dst));

    //PRINT
    for(auto item:dst)
        std::cout<<item<<" ";

    return 0;
}

3
Je ne pense pas que ce soit plus facile à utiliser std::vector::insert, mais cela fait quelque chose de différent: fusionner deux gammes dans une nouvelle gamme vs insérer un vecteur à la fin d'un autre. Vaut-il la peine de mentionner dans la réponse?
jb

4

Si votre objectif est simplement d'itérer sur la plage de valeurs à des fins de lecture seule, une alternative consiste à enrouler les deux vecteurs autour d'un proxy (O (1)) au lieu de les copier (O (n)), afin qu'ils soient rapidement visibles comme un seul, contigu.

std::vector<int> A{ 1, 2, 3, 4, 5};
std::vector<int> B{ 10, 20, 30 };

VecProxy<int> AB(A, B);  // ----> O(1)!

for (size_t i = 0; i < AB.size(); i++)
    std::cout << AB[i] << " ";  // ----> 1 2 3 4 5 10 20 30

Reportez-vous à https://stackoverflow.com/a/55838758/2379625 pour plus de détails, y compris la mise en œuvre de «VecProxy» ainsi que les avantages et les inconvénients.


3
vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2 = {11, 12, 13, 14, 15};
copy(v2.begin(), v2.end(), back_inserter(v1));

6
Bien que cet extrait de code puisse résoudre le problème, il n'explique pas pourquoi ni comment il répond à la question. Veuillez inclure une explication pour votre code , car cela aide vraiment à améliorer la qualité de votre message. Flaggers / relecteurs: pour les réponses en code uniquement comme celle-ci, downvote, ne supprimez pas! (Remarque: Cette réponse peut en fait être assez simple pour rendre une explication, et donc des votes négatifs, inutile. Vous pouvez toujours ajouter une explication pour empêcher plus de drapeaux NAA / VLQ.)
Scott Weldon

2

J'ai implémenté cette fonction qui concatène n'importe quel nombre de conteneurs, passant de rvalue-references et copiant autrement

namespace internal {

// Implementation detail of Concatenate, appends to a pre-reserved vector, copying or moving if
// appropriate
template<typename Target, typename Head, typename... Tail>
void AppendNoReserve(Target* target, Head&& head, Tail&&... tail) {
    // Currently, require each homogenous inputs. If there is demand, we could probably implement a
    // version that outputs a vector whose value_type is the common_type of all the containers
    // passed to it, and call it ConvertingConcatenate.
    static_assert(
            std::is_same_v<
                    typename std::decay_t<Target>::value_type,
                    typename std::decay_t<Head>::value_type>,
            "Concatenate requires each container passed to it to have the same value_type");
    if constexpr (std::is_lvalue_reference_v<Head>) {
        std::copy(head.begin(), head.end(), std::back_inserter(*target));
    } else {
        std::move(head.begin(), head.end(), std::back_inserter(*target));
    }
    if constexpr (sizeof...(Tail) > 0) {
        AppendNoReserve(target, std::forward<Tail>(tail)...);
    }
}

template<typename Head, typename... Tail>
size_t TotalSize(const Head& head, const Tail&... tail) {
    if constexpr (sizeof...(Tail) > 0) {
        return head.size() + TotalSize(tail...);
    } else {
        return head.size();
    }
}

}  // namespace internal

/// Concatenate the provided containers into a single vector. Moves from rvalue references, copies
/// otherwise.
template<typename Head, typename... Tail>
auto Concatenate(Head&& head, Tail&&... tail) {
    size_t totalSize = internal::TotalSize(head, tail...);
    std::vector<typename std::decay_t<Head>::value_type> result;
    result.reserve(totalSize);
    internal::AppendNoReserve(&result, std::forward<Head>(head), std::forward<Tail>(tail)...);
    return result;
}

1

Si ce que vous cherchez est un moyen d'ajouter un vecteur à un autre après la création, vector::insertc'est votre meilleur pari, comme cela a été répondu plusieurs fois, par exemple:

vector<int> first = {13};
const vector<int> second = {42};

first.insert(first.end(), second.cbegin(), second.cend());

Malheureusement, il n'y a aucun moyen de construire un const vector<int>, comme ci-dessus, vous devez construire et ensuite insert.


Si ce que vous recherchez réellement est un conteneur pour contenir la concaténation de ces deux vector<int>s, il peut y avoir quelque chose de mieux à votre disposition, si:

  1. Votre vector contient des primitives
  2. Vos primitives contenues sont de taille 32 bits ou moins
  3. Vous voulez un constconteneur

Si tout ce qui précède est vrai, je vous suggère d'utiliser basic_stringqui char_typecorrespond à la taille de la primitive contenue dans votre vector. Vous devez inclure un static_assertdans votre code pour valider la cohérence de ces tailles:

static_assert(sizeof(char32_t) == sizeof(int));

Avec cette tenue vraie, vous pouvez simplement faire:

const u32string concatenation = u32string(first.cbegin(), first.cend()) + u32string(second.cbegin(), second.cend());

Pour plus d'informations sur les différences entre stringet vectorvous pouvez consulter ici: https://stackoverflow.com/a/35558008/2642059

Pour un exemple en direct de ce code, vous pouvez regarder ici: http://ideone.com/7Iww3I


0

Cette solution peut être un peu compliquée, mais elle boost-rangea aussi d'autres belles choses à offrir.

#include <iostream>
#include <vector>
#include <boost/range/algorithm/copy.hpp>

int main(int, char**) {
    std::vector<int> a = { 1,2,3 };
    std::vector<int> b = { 4,5,6 };
    boost::copy(b, std::back_inserter(a));
    for (auto& iter : a) {
        std::cout << iter << " ";
    }
    return EXIT_SUCCESS;
}

Souvent, l'intention est de combiner le vecteur aet de bsimplement le répéter en effectuant une opération. Dans ce cas, il y a la joinfonction simple ridicule .

#include <iostream>
#include <vector>
#include <boost/range/join.hpp>
#include <boost/range/algorithm/copy.hpp>

int main(int, char**) {
    std::vector<int> a = { 1,2,3 };
    std::vector<int> b = { 4,5,6 };
    std::vector<int> c = { 7,8,9 };
    // Just creates an iterator
    for (auto& iter : boost::join(a, boost::join(b, c))) {
        std::cout << iter << " ";
    }
    std::cout << "\n";
    // Can also be used to create a copy
    std::vector<int> d;
    boost::copy(boost::join(a, boost::join(b, c)), std::back_inserter(d));
    for (auto& iter : d) {
        std::cout << iter << " ";
    }
    return EXIT_SUCCESS;
}

Pour les grands vecteurs, cela peut être un avantage, car il n'y a pas de copie. Il peut également être utilisé pour copier facilement une généralisation dans plusieurs conteneurs.

Pour une raison quelconque, il n'y a rien de tel boost::join(a,b,c), ce qui pourrait être raisonnable.


0

Vous pouvez le faire avec des algorithmes STL pré-implémentés en utilisant un modèle pour une utilisation de type polymorphe.

#include <iostream>
#include <vector>
#include <algorithm>

template<typename T>

void concat(std::vector<T>& valuesa, std::vector<T>& valuesb){

     for_each(valuesb.begin(), valuesb.end(), [&](int value){ valuesa.push_back(value);});
}

int main()
{
    std::vector<int> values_p={1,2,3,4,5};
    std::vector<int> values_s={6,7};

   concat(values_p, values_s);

    for(auto& it : values_p){

        std::cout<<it<<std::endl;
    }

    return 0;
}

Vous pouvez effacer le deuxième vecteur si vous ne souhaitez pas l'utiliser davantage ( clear()méthode).


-1

Concatène deux std::vector-savec forboucle en un std::vector.

Exemple:

std::vector <int> v1 {1, 2, 3};//declare vector1
std::vector <int> v2 {4, 5};//declare vector2
std::vector <int> suma;//declare vector suma

for(int i = 0; i < v1.size();i++)//for loop 1
{
     suma.push_back(v1[i]);
}

for(int i = 0; i< v2.size();i++)/for loop 2
{
     suma.push_back(v2[i]);
}

for(int i = 0; i < suma.size(); i++)//for loop 3-output
{
     std::cout<<suma[i];
}

Écrivez ce code main().


Vous forboucles avez tort. Les index valides dans un vecteur sont de 0 à size()-1. Vos conditions de résiliation de boucle doivent être i < v1.size(), en utilisant <not <=. Une mauvaise condition permet d'accéder à la mémoire en dehors du conteneur.
Blastfurnace

en plus de ne pas fonctionner, ce code est fortement non idiomatique. Vous devriez au moins utiliser des autoitérateurs au lieu d'une indexation manuelle. Vous ne vous souciez pas de l'index que vous concaténez, mais du fait qu'il est fait de manière séquentielle.
Tarick Welling

Pouvez-vous expliquer pourquoi vous utilisez size()-1dans deux de vos conditions de boucle? Cela ignore les derniers éléments vectoriels. La troisième boucle est la seule correcte maintenant.
Blastfurnace

-3

Pour être honnête, vous pouvez rapidement concaténer deux vecteurs en copiant des éléments de deux vecteurs dans l'autre ou simplement ajouter un seul des deux vecteurs!. Cela dépend de votre objectif.

Méthode 1: attribuer un nouveau vecteur à sa taille est la somme de la taille de deux vecteurs d'origine.

vector<int> concat_vector = vector<int>();
concat_vector.setcapacity(vector_A.size() + vector_B.size());
// Loop for copy elements in two vectors into concat_vector

Méthode 2: ajouter le vecteur A en ajoutant / insérant des éléments du vecteur B.

// Loop for insert elements of vector_B into vector_A with insert() 
function: vector_A.insert(vector_A .end(), vector_B.cbegin(), vector_B.cend());

4
Qu'ajoute votre réponse qui n'a pas déjà été fournie dans d'autres réponses?
mat

13
@Mat: caractères gras.
marcv81

Si le ou les vecteurs d'origine ne sont plus nécessaires après, il peut être préférable de les utiliser std::move_iteratorpour que les éléments soient déplacés plutôt que copiés. (voir en.cppreference.com/w/cpp/iterator/move_iterator ).
tmlen

Qu'est-ce que c'est setcapacity? Qu'est-ce que c'est function: ?
LF

@LF Je pense qu'il parle de la resizeméthode.
Matthieu H
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.