Quelles sont les utilisations du «placement nouveau»?


411

Est-ce que quelqu'un ici a déjà utilisé le "placement nouveau" de C ++? Si oui, pourquoi? Il me semble que cela ne serait utile que sur du matériel mappé en mémoire.


14
Ce sont juste les informations que je cherchais, pour appeler les constructeurs d'objets sur les pools de mémoire alloués boost. (En espérant que ces mots clés faciliteront la recherche de quelqu'un à l'avenir).
Sideshow Bob

2
Il est utilisé dans l'article Wikipedia C ++ 11 dans le constructeur d'une union.
HelloGoodbye

@HelloGoodbye, intéressant! Dans l'article que vous avez lié, pourquoi ne pouvez-vous pas simplement p = ptutiliser et utiliser l'opérateur d'affectation Pointau lieu de le faire new(&p) Point(pt)? Je m'interroge sur les différences entre les deux. Le premier ferait-il appel operator=à Point, tandis que le second appellerait constructeur de copie Point? mais je ne sais toujours pas pourquoi l'un est meilleur que l'autre.
Andrei-Niculae Petre

@ Andrei-NiculaePetre Je n'ai pas utilisé placement new moi-même, mais je suppose que vous devriez l'utiliser - avec le constructeur de copie - si vous n'avez actuellement aucun objet de cette classe, sinon vous devriez utiliser l'opérateur d'affectation de copie. Sauf si la classe est triviale; alors peu importe lequel d'entre eux vous utilisez. La même chose vaut pour la destruction de l'objet. Ne pas gérer cela correctement pour les classes non triviales peut très probablement conduire à un comportement étrange, et peut même provoquer un comportement non défini dans certaines situations.
HelloGoodbye

@ Andrei-NiculaePetre En fait, je trouve l' exemple dans l'article Wikipedia assez mauvais, car il suppose simplement qu'aucun objet antérieur n'existe et qu'ils doivent en construire un. Ce n'est pas le cas si U::operator=vient d'être appelé.
HelloGoodbye

Réponses:


365

Placement new vous permet de construire un objet en mémoire déjà alloué.

Vous pouvez le faire pour l'optimisation lorsque vous devez construire plusieurs instances d'un objet, et il est plus rapide de ne pas réallouer de la mémoire chaque fois que vous avez besoin d'une nouvelle instance. Au lieu de cela, il peut être plus efficace d'effectuer une seule allocation pour un bloc de mémoire pouvant contenir plusieurs objets, même si vous ne souhaitez pas utiliser tout cela à la fois.

DevX donne un bon exemple :

Le C ++ standard prend également en charge le nouvel opérateur de placement, qui construit un objet sur un tampon préalloué. Ceci est utile lors de la création d'un pool de mémoire, d'un garbage collector ou simplement lorsque les performances et la sécurité des exceptions sont primordiales (il n'y a aucun risque d'échec d'allocation car la mémoire a déjà été allouée et la construction d'un objet sur un tampon pré-alloué prend moins de temps) :

char *buf  = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi");    // placement new
string *q = new string("hi");          // ordinary heap allocation

Vous pouvez également vous assurer qu'il ne peut y avoir aucun échec d'allocation à une certaine partie du code critique (par exemple, dans le code exécuté par un stimulateur cardiaque). Dans ce cas, vous souhaitez allouer de la mémoire plus tôt, puis utiliser le placement nouveau dans la section critique.

Désallocation en placement nouveau

Vous ne devez pas désallouer tous les objets qui utilisent le tampon de mémoire. Au lieu de cela, vous devez supprimer [] uniquement le tampon d'origine. Vous devrez alors appeler manuellement les destructeurs de vos classes. Pour une bonne suggestion à ce sujet, veuillez consulter la FAQ de Stroustrup sur: Y a - t-il une "suppression de placement" ?


54
Ce n'est pas obsolète car vous avez besoin de cette fonctionnalité pour implémenter efficacement les objets conteneurs (comme le vecteur). Si vous ne construisez pas votre propre conteneur, vous n'avez pas besoin d'utiliser cette fonctionnalité.
Martin York

26
Il est également très important de ne pas oublier # d'inclure <memory>, sinon vous pourriez rencontrer de terribles maux de tête sur certaines plateformes qui ne reconnaissent pas automatiquement le placement nouveau
Ramon Zarazua B.

22
Strictement, c'est un comportement indéfini d'appeler delete[]le chartampon d' origine . L'utilisation du placement newa mis fin à la durée de vie des charobjets d' origine en réutilisant leur stockage. Si vous appelez maintenant delete[] bufle type dynamique des objets pointés vers, ils ne correspondent plus à leur type statique, vous avez donc un comportement indéfini. Il est plus cohérent d'utiliser operator new/ operator deleted'allouer de la mémoire brute destinée à être utilisée par placement new.
CB Bailey

31
Je sauterais certainement d'utiliser le tas dans un stimulateur cardiaque :-)
Eli Bendersky

15
@RamonZarazua Mauvais en-tête, c'est #include <new>.
bit2shift

63

Nous l'utilisons avec des pools de mémoire personnalisés. Juste un croquis:

class Pool {
public:
    Pool() { /* implementation details irrelevant */ };
    virtual ~Pool() { /* ditto */ };

    virtual void *allocate(size_t);
    virtual void deallocate(void *);

    static Pool::misc_pool() { return misc_pool_p; /* global MiscPool for general use */ }
};

class ClusterPool : public Pool { /* ... */ };
class FastPool : public Pool { /* ... */ };
class MapPool : public Pool { /* ... */ };
class MiscPool : public Pool { /* ... */ };

// elsewhere...

void *pnew_new(size_t size)
{
   return Pool::misc_pool()->allocate(size);
}

void *pnew_new(size_t size, Pool *pool_p)
{
   if (!pool_p) {
      return Pool::misc_pool()->allocate(size);
   }
   else {
      return pool_p->allocate(size);
   }
}

void pnew_delete(void *p)
{
   Pool *hp = Pool::find_pool(p);
   // note: if p == 0, then Pool::find_pool(p) will return 0.
   if (hp) {
      hp->deallocate(p);
   }
}

// elsewhere...

class Obj {
public:
   // misc ctors, dtors, etc.

   // just a sampling of new/del operators
   void *operator new(size_t s)             { return pnew_new(s); }
   void *operator new(size_t s, Pool *hp)   { return pnew_new(s, hp); }
   void operator delete(void *dp)           { pnew_delete(dp); }
   void operator delete(void *dp, Pool*)    { pnew_delete(dp); }

   void *operator new[](size_t s)           { return pnew_new(s); }
   void *operator new[](size_t s, Pool* hp) { return pnew_new(s, hp); }
   void operator delete[](void *dp)         { pnew_delete(dp); }
   void operator delete[](void *dp, Pool*)  { pnew_delete(dp); }
};

// elsewhere...

ClusterPool *cp = new ClusterPool(arg1, arg2, ...);

Obj *new_obj = new (cp) Obj(arg_a, arg_b, ...);

Vous pouvez maintenant regrouper des objets dans une seule arène de mémoire, sélectionner un allocateur qui est très rapide mais sans désallocation, utiliser le mappage de mémoire et toute autre sémantique que vous souhaitez imposer en choisissant le pool et en le passant comme argument au placement d'un objet. nouvel opérateur.


1
Oui. Nous devenons assez intelligents à ce sujet, mais c'est hors sujet pour cette question.
Don Wakefield

2
@jdkoftinoff avez-vous un lien vers un exemple de code réel? semble assez intéressant pour moi!
Victor

@DonWakefield Comment gérez-vous l'alignement dans ce pool? Ne devriez-vous pas passer l'alignement comme argument à allocate()quelque part?
Mikhail Vasilyev

1
@MikhailVasilyev, dans une vraie implémentation, vous géreriez bien sûr cela. Exemple de code uniquement.
Don Wakefield

que faire si l'emplacement est une adresse non valide, dites 0x0?
Charlie

51

C'est utile si vous voulez séparer l'allocation de l'initialisation. STL utilise placement new pour créer des éléments de conteneur.


35

Je l'ai utilisé dans la programmation en temps réel. Nous ne voulons généralement pas effectuer d'allocation dynamique (ou de désallocation) après le démarrage du système, car il n'y a aucune garantie de la durée de cette opération.

Ce que je peux faire, c'est préallouer une grande partie de la mémoire (suffisamment grande pour contenir tout ce dont la classe peut avoir besoin). Ensuite, une fois que j'ai compris au moment de l'exécution comment construire les choses, le placement nouveau peut être utilisé pour construire des objets là où je les veux. Je sais que je l'ai utilisé dans une situation pour aider à créer un tampon circulaire hétérogène .

Ce n'est certainement pas pour les âmes sensibles, mais c'est pourquoi ils en font un peu la syntaxe.


Salut TED, pourriez-vous s'il vous plaît partager plus sur la solution que vous avez. Je pense à une solution pré-allouée mais je n'ai pas beaucoup progressé. Merci d'avance!
Viet

1
Eh bien, le code tampon circulaire hétrogène réel était vraiment la partie la plus délicate à obtenir. Le palcement nouveau semble un peu macabre, mais en comparaison, cela n'a posé aucun problème.
TED

26

Je l'ai utilisé pour construire des objets alloués sur la pile via alloca ().

fiche sans vergogne: j'ai blogué à ce sujet ici .


article intéressant, mais je ne suis pas sûr de comprendre l'avantage de l'utiliser sur boost::array. Pouvez-vous développer un peu cela?
GrahamS

boost :: array requiert que la taille du tableau soit une constante de temps de compilation. Cela n'a pas cette limitation.
Ferruccio

2
@Ferruccio C'est assez cool, j'ai remarqué que votre macro est légèrement dangereuse, à savoir que la taille pourrait être une expression. Si x + 1 est transmis par exemple, vous le développeriez à sizeof (type) * x + 1, ce qui serait incorrect. Vous devez encadrer votre macro pour la rendre plus sûre.
Benj

Utiliser avec alloca me semble dangereux si une exception est levée car vous devez appeler les destructeurs sur tous vos objets.
CashCow

14

Geek en chef: BINGO! Vous l'avez totalement compris - c'est exactement ce pour quoi il est parfait. Dans de nombreux environnements embarqués, les contraintes externes et / ou le scénario d'utilisation globale obligent le programmeur à séparer l'allocation d'un objet de son initialisation. Ensemble, C ++ appelle cette "instanciation"; mais chaque fois que l'action du constructeur doit être explicitement invoquée SANS allocation dynamique ou automatique, le placement nouveau est le moyen de le faire. C'est également le moyen idéal pour localiser un objet C ++ global qui est épinglé à l'adresse d'un composant matériel (E / S mappées en mémoire), ou pour tout objet statique qui, pour une raison quelconque, doit résider à une adresse fixe.


12

Je l'ai utilisé pour créer une classe Variant (c'est-à-dire un objet qui peut représenter une valeur unique qui peut être l'un d'un certain nombre de types différents).

Si tous les types de valeurs pris en charge par la classe Variant sont des types POD (par exemple, int, float, double, bool), une union de style C balisée est suffisante, mais si vous voulez que certains types de valeurs soient des objets C ++ ( par exemple std :: string), la fonction d'union C ne fera pas l'affaire, car les types de données non POD ne peuvent pas être déclarés comme faisant partie d'une union.

Donc, à la place, j'alloue un tableau d'octets suffisamment grand (par exemple sizeof (the_largest_data_type_I_support)) et j'utilise placement new pour initialiser l'objet C ++ approprié dans cette zone lorsque le Variant est défini pour contenir une valeur de ce type. (Et l'emplacement doit être supprimé au préalable lors de la transition d'un autre type de données non POD, bien sûr)


Euh, les types de données non-POD peuvent être déclarés dans une union, tant que vous fournissez un ctor d'union - et bon - ce ctor utiliserait probablement le placementnew pour initialiser sa sous-classe non-POD. Ref: stackoverflow.com/a/33289972/2757035 Réinventer cette roue en utilisant un tableau d'octets arbitrairement grand est un morceau acrobatique impressionnant mais semble totalement inutile, Alors, qu'ai-je manqué? :)
underscore_d

6
Vous avez manqué toutes les versions de C ++ avant C ++ 11, qui dans de nombreux cas doivent encore être prises en charge. :)
Jeremy Friesner

10

Placement new est également très utile lors de la sérialisation (par exemple avec boost :: serialization). En 10 ans de c ++, ce n'est que le deuxième cas pour lequel j'ai eu besoin d'un nouveau placement (troisième si vous incluez des interviews :)).


9

Il est également utile lorsque vous souhaitez réinitialiser des structures globales ou allouées statiquement.

L'ancienne façon C utilisait memset() pour définir tous les éléments à 0. Vous ne pouvez pas le faire en C ++ en raison de vtables et de constructeurs d'objets personnalisés.

J'utilise donc parfois ce qui suit

 static Mystruct m;

 for(...)  {
     // re-initialize the structure. Note the use of placement new
     // and the extra parenthesis after Mystruct to force initialization.
     new (&m) Mystruct();

     // do-some work that modifies m's content.
 }

1
N'auriez-vous pas besoin de faire une destruction correspondante avant de le réinitialiser de cette façon?
Head Geek

[Modifié pour l'orthographe] Habituellement - vous le faites. Mais parfois, lorsque vous savez que la classe n'alloue pas de mémoire ou d'autres ressources (ou que vous les désallouez en externe - par exemple lorsque vous utilisez des pools de mémoire), vous pouvez utiliser cette technique. Cela garantit que les pointeurs v-table ne sont pas écrasés. - nimrodm Il y a 16 heures
nimrodm

1
Même en C, l'utilisation de la mise à 0 de tous les bits n'est garantie que pour produire une représentation de 0 pour les types intégraux, pas pour les autres types (le pointeur nul peut avoir une représentation non nulle).
curiousguy

@curiousguy - pour les types primitifs, vous avez raison (cela rendra le programme prévisible, ce qui est un avantage en matière de débogage). Cependant, les types de données C ++ verront leur constructeur s'exécuter (sur place) et seront correctement initialisés.
nimrodm

9

Je pense que cela n'a été mis en évidence par aucune réponse, mais un autre bon exemple et utilisation pour le nouveau placement est de réduire la fragmentation de la mémoire (en utilisant des pools de mémoire). Ceci est particulièrement utile dans les systèmes embarqués et à haute disponibilité. Dans ce dernier cas, c'est particulièrement important car pour un système qui doit fonctionner 24/365 jours, il est très important de ne pas avoir de fragmentation. Ce problème n'a rien à voir avec une fuite de mémoire.

Même lorsqu'une très bonne implémentation malloc est utilisée (ou une fonction de gestion de mémoire similaire), il est très difficile de gérer la fragmentation pendant longtemps. À un moment donné, si vous ne gérez pas intelligemment les appels de réservation / libération de mémoire, vous pourriez vous retrouver avec beaucoup de petites lacunes qui sont difficiles à réutiliser (attribuer à de nouvelles réservations). Ainsi, l'une des solutions utilisées dans ce cas consiste à utiliser un pool de mémoire pour allouer au préalable la mémoire pour les objets d'application. Après chaque fois que vous avez besoin de mémoire pour un objet, vous utilisez simplement le nouvel emplacement pour créer un nouvel objet sur la mémoire déjà réservée.

De cette façon, une fois que votre application démarre, vous disposez déjà de toute la mémoire nécessaire. Toutes les nouvelles réservations / versions de mémoire vont aux pools alloués (vous pouvez avoir plusieurs pools, un pour chaque classe d'objet différente). Aucune fragmentation de la mémoire ne se produit dans ce cas car il n'y aura pas de lacunes et votre système peut fonctionner pendant de très longues périodes (années) sans souffrir de fragmentation.

J'ai vu cela dans la pratique spécialement pour le VxWorks RTOS car son système d'allocation de mémoire par défaut souffre beaucoup de la fragmentation. Ainsi, l'allocation de mémoire via la méthode standard new / malloc était fondamentalement interdite dans le projet. Toutes les réservations de mémoire doivent aller dans un pool de mémoire dédié.


9

Il est en fait nécessaire d'implémenter tout type de structure de données qui alloue plus de mémoire que le minimum requis pour le nombre d'éléments insérés (c'est-à-dire autre chose qu'une structure liée qui alloue un nœud à la fois).

Prenez des conteneurs aiment unordered_map, vectorou deque. Tous ces éléments allouent plus de mémoire que ce qui est minimalement requis pour les éléments que vous avez insérés jusqu'à présent pour éviter d'exiger une allocation de segment de mémoire pour chaque insertion unique. Prenons l' vectorexemple le plus simple.

Quand vous faites:

vector<Foo> vec;

// Allocate memory for a thousand Foos:
vec.reserve(1000);

... qui ne construit pas réellement mille Foos. Il leur alloue / réserve simplement de la mémoire. Si vous vectorn'utilisiez pas le placement nouveau ici, ce serait une construction par défaut Foospartout et vous devrez invoquer leurs destructeurs même pour les éléments que vous n'avez même jamais insérés en premier lieu.

Allocation! = Construction, Libération! = Destruction

D'une manière générale, pour implémenter de nombreuses structures de données comme ci-dessus, vous ne pouvez pas traiter l'allocation de mémoire et la construction d'éléments comme une chose indivisible, et vous ne pouvez pas non plus traiter la libération de mémoire et la destruction d'éléments comme une chose indivisible.

Il doit y avoir une séparation entre ces idées pour éviter d'invoquer inutilement des constructeurs et des destructeurs à gauche et à droite, et c'est pourquoi la bibliothèque standard sépare l'idée de std::allocator(qui ne construit pas ou ne détruit pas les éléments lorsqu'elle alloue / libère de la mémoire *) loin de les conteneurs qui l'utilisent qui construisent manuellement des éléments en utilisant placement new et détruisent manuellement des éléments en utilisant des invocations explicites de destructeurs.

  • Je déteste le design de std::allocatormais c'est un sujet différent sur lequel j'éviterai de parler. :-RÉ

Donc, de toute façon, j'ai tendance à l'utiliser beaucoup car j'ai écrit un certain nombre de conteneurs C ++ standard à usage général qui ne pouvaient pas être construits en fonction des conteneurs existants. Parmi eux, une petite implémentation vectorielle que j'ai construite il y a quelques décennies pour éviter les allocations de tas dans les cas courants, et un tri efficace en mémoire (n'alloue pas un nœud à la fois). Dans les deux cas, je ne pouvais pas vraiment les implémenter en utilisant les conteneurs existants, et j'ai donc dû placement newéviter d'utiliser des constructeurs et des destructeurs superflus sur des choses inutiles à gauche et à droite.

Naturellement, si vous travaillez avec des allocateurs personnalisés pour allouer des objets individuellement, comme une liste gratuite, vous voudrez également généralement utiliser placement new, comme ceci (exemple de base qui ne dérange pas avec la sécurité d'exception ou RAII):

Foo* foo = new(free_list.allocate()) Foo(...);
...
foo->~Foo();
free_list.free(foo);

8

C'est utile si vous construisez un noyau - où placez-vous le code du noyau que vous lisez à partir du disque ou de la page? Vous devez savoir où aller.

Ou dans d'autres circonstances très rares, comme lorsque vous avez beaucoup de place allouée et que vous souhaitez placer quelques structures les unes derrière les autres. Ils peuvent être compressés de cette façon sans avoir besoin de l'opérateur offsetof (). Il existe cependant d'autres astuces pour cela.

Je crois également que certaines implémentations STL utilisent le placement new, comme std :: vector. Ils allouent de la place pour 2 ^ n éléments de cette façon et n'ont pas besoin de toujours se réallouer.


La réduction des allocations de mémoire est l'une des principales raisons de l'utiliser, ainsi que des "astuces" comme le chargement d'objets hors du disque
lefticus

Je ne connais aucun noyau écrit en C ++; la plupart des noyaux sont écrits en C droit
Adam Rosenfield

8
Le système d'exploitation avec lequel j'ai appris les bases du système d'exploitation est écrit en C ++: sweb.sourceforge.net
mstrobl

8

Il est utilisé par std::vector<>car std::vector<>alloue généralement plus de mémoire qu'il n'y en a objectsdans le vector<>.


7

Je l'ai utilisé pour stocker des objets avec des fichiers mappés en mémoire.
L'exemple spécifique était une base de données d'images qui traitait un très grand nombre de grandes images (plus que ce qui pouvait tenir en mémoire).


7

Je l'ai vu utilisé comme un léger hack de performance pour un pointeur de "type dynamique" (dans la section "Sous le capot"):

Mais voici l'astuce que j'ai utilisée pour obtenir des performances rapides pour les petits types: si la valeur détenue peut tenir à l'intérieur d'un vide *, je ne prends pas la peine d'allouer un nouvel objet, je le force dans le pointeur lui-même en utilisant placement new .


Que signifie si la valeur détenue peut tenir à l'intérieur d'un vide * signifie? Il est toujours possible d'assigner n'importe quel type de pointeur à void *. Pouvez-vous s'il vous plaît nous montrer un exemple?
anurag86

@ anurag86: Sur ma machine 64 bits, un void*prend 8 octets. Il est un peu idiot de pointer un octet void*sur un octet bool. Mais il est tout à fait possible de superposer le boolsur le void*, un peu comme un union { bool b; void* v }. Vous avez besoin d'un moyen de savoir que ce que vous appelez un void*est en fait un bool(ou un short, ou un float, etc.). L'article auquel j'ai lié décrit comment procéder. Et, pour répondre à la question d'origine, le placement newest la fonctionnalité utilisée pour créer un bool(ou un autre type) où un void*est attendu, (les transtypages sont utilisés pour obtenir / modifier la valeur ultérieurement).
Max Lybbert

@ anurag86: Ce n'est pas la même chose, mais vous pouvez être intéressé par des pointeurs balisés ( en.wikipedia.org/wiki/Tagged_pointer ).
Max Lybbert

6

Je l'ai utilisé pour créer des objets basés sur la mémoire contenant des messages reçus du réseau.


5

Généralement, le placement nouveau est utilisé pour se débarrasser du coût d'allocation d'un «nouveau normal».

Un autre scénario où je l'ai utilisé est un endroit où je voulais avoir accès au pointeur sur un objet qui devait encore être construit, pour implémenter un singleton par document.



4

Le seul endroit que j'ai rencontré est dans des conteneurs qui allouent un tampon contigu, puis le remplissent d'objets selon les besoins. Comme mentionné, std :: vector peut le faire, et je sais que certaines versions de MFC CArray et / ou CList l'ont fait (parce que c'est là que je l'ai rencontré pour la première fois). La méthode de surallocation du tampon est une optimisation très utile, et le placement nouveau est à peu près le seul moyen de construire des objets dans ce scénario. Il est également parfois utilisé pour construire des objets dans des blocs de mémoire alloués en dehors de votre code direct.

Je l'ai utilisé à un titre similaire, bien qu'il ne revienne pas souvent. C'est un outil utile pour la boîte à outils C ++, cependant.


4

Les moteurs de script peuvent l'utiliser dans l'interface native pour allouer des objets natifs à partir de scripts. Voir Angelscript (www.angelcode.com/angelscript) pour des exemples.


3

Voir le fichier fp.h dans le projet xll à http://xll.codeplex.com Il résout le problème de "chumminess injustifié avec le compilateur" pour les tableaux qui aiment transporter leurs dimensions avec eux.

typedef struct _FP
{
    unsigned short int rows;
    unsigned short int columns;
    double array[1];        /* Actually, array[rows][columns] */
} FP;

2

Voici l'utilisation de tueur pour le constructeur sur place C ++: l'alignement sur une ligne de cache, ainsi que d'autres pouvoirs de 2 limites. Voici mon algorithme d'alignement de pointeur ultra-rapide à n'importe quelle puissance de 2 frontières avec 5 instructions de cycle unique ou moins :

/* Quickly aligns the given pointer to a power of two boundary IN BYTES.
@return An aligned pointer of typename T.
@brief Algorithm is a 2's compliment trick that works by masking off
the desired number in 2's compliment and adding them to the
pointer.
@param pointer The pointer to align.
@param boundary_byte_count The boundary byte count that must be an even
power of 2.
@warning Function does not check if the boundary is a power of 2! */
template <typename T = char>
inline T* AlignUp(void* pointer, uintptr_t boundary_byte_count) {
  uintptr_t value = reinterpret_cast<uintptr_t>(pointer);
  value += (((~value) + 1) & (boundary_byte_count - 1));
  return reinterpret_cast<T*>(value);
}

struct Foo { Foo () {} };
char buffer[sizeof (Foo) + 64];
Foo* foo = new (AlignUp<Foo> (buffer, 64)) Foo ();

Maintenant, cela ne vous fait-il pas sourire (:-). I ♥♥♥ C ++ 1x

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.