Określanie 32 vs 64 bit w C ++


136

Szukam sposobu, aby wiarygodnie określić, czy kod C ++ jest kompilowany w wersji 32 vs 64 bit. Wymyśliliśmy to, co uważamy za rozsądne rozwiązanie przy użyciu makr, ale byliśmy ciekawi, czy ludzie mogą wymyślić przypadki, w których może się to nie udać, lub czy istnieje lepszy sposób na zrobienie tego. Należy pamiętać, że próbujemy to zrobić w wieloplatformowym środowisku kompilatorów.

#if ((ULONG_MAX) == (UINT_MAX))
# define IS32BIT
#else
# define IS64BIT
#endif

#ifdef IS64BIT
DoMy64BitOperation()
#else
DoMy32BitOperation()
#endif

Dzięki.


8
Jeśli naprawdę zależy ci na wielkości słowa w twojej architekturze, nie zapomnij o możliwości, że nie jest to ani 32, ani 64-bitowa. Wiesz, istnieją architektury 16- i 128-bitowe.
alex tingle

Jaka jest różnica między operacją 64-bitową a 32-bitową?
peterchen

2
Naprawdę nie powinieneś uwarunkować tego na szerokości słowa platformy docelowej. Zamiast tego użyj rozmiaru odpowiednich typów danych bezpośrednio, aby określić, co należy zrobić. stdint.hmoże być twoim przyjacielem lub być może będziesz musiał opracować własne, odpowiednie typy.
Phil Miller,

Ten test nie działa w programie Visual Studio 2008 z dodatkiem SP1. Utknie na „IS64BIT” zarówno dla wersji 32-bitowej, jak i 64-bitowej.
Contango,

Odpowiedzi:


99

Niestety nie ma makra wieloplatformowego, które definiuje 32/64 bity w głównych kompilatorach. Najskuteczniejszy sposób, aby to zrobić, jest następujący.

Najpierw wybieram własną reprezentację. Wolę ŚRODOWISKO64 / ŚRODOWISKO32. Następnie dowiaduję się, czego używają wszystkie główne kompilatory do określenia, czy jest to środowisko 64-bitowe, czy nie, i używam tego do ustawiania moich zmiennych.

// Check windows
#if _WIN32 || _WIN64
#if _WIN64
#define ENVIRONMENT64
#else
#define ENVIRONMENT32
#endif
#endif

// Check GCC
#if __GNUC__
#if __x86_64__ || __ppc64__
#define ENVIRONMENT64
#else
#define ENVIRONMENT32
#endif
#endif

Inną łatwiejszą drogą jest po prostu ustawienie tych zmiennych z wiersza poleceń kompilatora.


3
cóż, istnieją inne kompilatory oprócz GCC i VS. Na przykład przychodzą na myśl QNX i GHS (chociaż podejrzewam, że QNX ma podobne definicje czasu kompilacji do GCC). Zapomniałeś również o architekturach MIPS64 i IA64 w swoim czeku GCC
Rom

14
@Rom, zdecydowanie więcej niż 2 kompilatory i architektury. To ma być tylko próbka podejścia do tego problemu, a nie pełne rozwiązanie.
JaredPar

2
Mówię „zwykle”. „Idealnie” jest prawdopodobnie bardziej realistyczne.
Steve Jessop

7
Myślę, że powinieneś użyć „#if zdefiniowane ( WIN32 ) || zdefiniowane (_WIN64)” itp.
KindDragon

3
#if _WIN32 || _WIN64... #elif __GNUC__... #else # error "Missing feature-test macro for 32/64-bit on this compiler."?
Davislor

100
template<int> void DoMyOperationHelper();

template<> void DoMyOperationHelper<4>() 
{
  // do 32-bits operations
}

template<> void DoMyOperationHelper<8>() 
{
  // do 64-bits operations
}

// helper function just to hide clumsy syntax
inline void DoMyOperation() { DoMyOperationHelper<sizeof(size_t)>(); }

int main()
{
  // appropriate function will be selected at compile time 
  DoMyOperation(); 

  return 0;
}

2
Co się stanie, jeśli size_t nie wynosi ani 4, ani 8?
Jesper

16
@Jesper, wtedy w powyższym przykładzie pojawi się błąd linku. Lub możesz wdrożyć DoMyOperation w tym przypadku
Kirill V. Lyadvinsky

1
Zręczne użycie szablonów i pochwały do ​​testowania tego, co ma znaczenie (rozmiar określonego typu), a nie korelacji.
Phil Miller,

2
Ostrożnie używaj do tego size_t. Możesz mieć problemy, gdy nie odpowiada on na przykład rozmiarowi wskaźnika (np. Na platformach z więcej niż jednym rozmiarem wskaźnika).
Logan Capaldo

8
Standard mówi, że rozmiar size_tjest wystarczająco duży, aby pomieścić rozmiar dowolnego przydzielonego obiektu w systemie. Zwykle jest to to, co chcesz wiedzieć podczas kompilacji warunkowej. Jeśli tego nie chcesz, możesz użyć tego fragmentu z innym typem zamiast size_t. Na przykład może to być void*.
Kirill V. Lyadvinsky

44

Niestety, w środowisku wieloplatformowym i kompilatorach wieloplatformowych nie ma jednej niezawodnej metody robienia tego wyłącznie w czasie kompilacji.

  • Zarówno _WIN32 i _WIN64 czasami może zarówno być niezdefiniowana, jeśli ustawienia projektu są wadliwe lub uszkodzone (szczególnie w Visual Studio 2008 SP1).
  • Projekt oznaczony jako „Win32” mógł być ustawiony na 64-bitowy z powodu błędu konfiguracji projektu.
  • W programie Visual Studio 2008 z dodatkiem SP1 czasami funkcja Intellisense nie powoduje wyszarzenia poprawnych części kodu, zgodnie z bieżącym #define. To sprawia, że ​​trudno jest zobaczyć, który #define jest używany w czasie kompilacji.

Dlatego jedyną niezawodną metodą jest połączenie 3 prostych kontroli :

  • 1) ustawienie czasu kompilacji oraz;
  • 2) sprawdzenie w czasie wykonywania oraz;
  • 3) Solidne sprawdzanie czasu kompilacji .

Proste sprawdzenie 1/3: ustawienie czasu kompilacji

Wybierz dowolną metodę, aby ustawić wymaganą zmienną #define. Proponuję metodę z @JaredPar:

// Check windows
#if _WIN32 || _WIN64
   #if _WIN64
     #define ENV64BIT
  #else
    #define ENV32BIT
  #endif
#endif

// Check GCC
#if __GNUC__
  #if __x86_64__ || __ppc64__
    #define ENV64BIT
  #else
    #define ENV32BIT
  #endif
#endif

Proste sprawdzenie 2/3: sprawdzenie w czasie wykonywania

W main () dwukrotnie sprawdź, czy sizeof () ma sens:

#if defined(ENV64BIT)
    if (sizeof(void*) != 8)
    {
        wprintf(L"ENV64BIT: Error: pointer should be 8 bytes. Exiting.");
        exit(0);
    }
    wprintf(L"Diagnostics: we are running in 64-bit mode.\n");
#elif defined (ENV32BIT)
    if (sizeof(void*) != 4)
    {
        wprintf(L"ENV32BIT: Error: pointer should be 4 bytes. Exiting.");
        exit(0);
    }
    wprintf(L"Diagnostics: we are running in 32-bit mode.\n");
#else
    #error "Must define either ENV32BIT or ENV64BIT".
#endif

Proste sprawdzenie 3/3: Solidne sprawdzanie czasu kompilacji

Ogólna zasada brzmi: „każdy #define musi kończyć się #else, który generuje błąd”.

#if defined(ENV64BIT)
    // 64-bit code here.
#elif defined (ENV32BIT)
    // 32-bit code here.
#else
    // INCREASE ROBUSTNESS. ALWAYS THROW AN ERROR ON THE ELSE.
    // - What if I made a typo and checked for ENV6BIT instead of ENV64BIT?
    // - What if both ENV64BIT and ENV32BIT are not defined?
    // - What if project is corrupted, and _WIN64 and _WIN32 are not defined?
    // - What if I didn't include the required header file?
    // - What if I checked for _WIN32 first instead of second?
    //   (in Windows, both are defined in 64-bit, so this will break codebase)
    // - What if the code has just been ported to a different OS?
    // - What if there is an unknown unknown, not mentioned in this list so far?
    // I'm only human, and the mistakes above would break the *entire* codebase.
    #error "Must define either ENV32BIT or ENV64BIT"
#endif

Aktualizacja 2017-01-17

Komentarz od @AI.G:

4 lata później (nie wiem, czy wcześniej było to możliwe) możesz przekonwertować sprawdzenie w czasie wykonywania na sprawdzenie kompilacji za pomocą statycznego assert: static_assert (sizeof (void *) == 4) ;. Teraz wszystko jest gotowe w czasie kompilacji :)

załącznik A

Nawiasem mówiąc, powyższe reguły można dostosować, aby cała baza kodu była bardziej niezawodna:

  • Każda instrukcja if () kończy się „else”, które generuje ostrzeżenie lub błąd.
  • Każda instrukcja switch () kończy się „default:”, które generuje ostrzeżenie lub błąd.

Powodem, dla którego to działa dobrze, jest to, że zmusza cię do przemyślenia każdego pojedynczego przypadku z wyprzedzeniem i nie polega na (czasem błędnej) logice w części „else”, aby wykonać poprawny kod.

Użyłem tej techniki (między innymi), aby napisać projekt 30 000 linii, który działał bezbłędnie od dnia pierwszego wdrożenia do produkcji (czyli 12 miesięcy temu).


sizeof(void*)czy jest rozwiązywany w czasie kompilacji czy w czasie wykonywania? jeśli jest w czasie kompilacji, to w czasie wykonywania sprawdzenie zawsze będzie if(8!=8){...}.
Ameen

@ameen Problem został rozwiązany w czasie wykonywania. Celem tego sprawdzenia jest upewnienie się, że program kończy pracę z odpowiednim błędem, jeśli bitowość nie jest zgodna z oczekiwaniami. Oznacza to, że programista może natychmiast naprawić ten błąd, zamiast próbować diagnozować subtelne błędy, które pojawiają się później.
Contango

3
4 lata później (nie wiem, czy to było możliwe wcześniej) można przekonwertować czek run-time do kompilacji za pomocą assert jeden statyczny: static_assert(sizeof(void*) == 4);. Teraz wszystko gotowe w czasie kompilacji :)
Al.G.

1
static_assert(sizeof(void*) * CHAR_BIT == 32)jest bardziej wyrazista i technicznie poprawna (chociaż nie znam żadnej architektury, w której bajty mają inną liczbę bitów niż 8)
Xeverous

1
Zobacz także moją odpowiedź poniżej, która łączy tę doskonałą odpowiedź z „ Better Macros, Better Flags ” z Fluent C ++.
metal

30

Powinieneś móc używać makr zdefiniowanych w stdint.h. W szczególności INTPTR_MAXjest to dokładnie taka wartość, jakiej potrzebujesz.

#include <cstdint>
#if INTPTR_MAX == INT32_MAX
    #define THIS_IS_32_BIT_ENVIRONMENT
#elif INTPTR_MAX == INT64_MAX
    #define THIS_IS_64_BIT_ENVIRONMENT
#else
    #error "Environment not 32 or 64-bit."
#endif

Niektóre (wszystkie?) Wersje kompilatora Microsoftu nie są dostarczane z stdint.h. Nie wiem dlaczego, ponieważ jest to standardowy plik. Oto wersja, której możesz użyć:http://msinttypes.googlecode.com/svn/trunk/stdint.h


4
Dlaczego nie ma stdint.h dla Microsoft? Ponieważ został wprowadzony ze standardem C99, a Microsoft wydaje się mieć aktywną niechęć do implementowania nawet najłatwiejszych rzeczy z C99. Nawet łatwa biblioteka, która nie wymaga zmiany kompilatora. Nawet rzeczy, które są już wykonywane podczas kompilacji dla C ++ (np. Deklaracje po instrukcjach). Wiem, że wymaga testów itp., Ale wiem też, że MS otrzymuje (lub kiedyś dostało) sporą część swojej biblioteki z Dinkumware / Plauger, a Dinkumware miał bibliotekę C99 od lat.
Michael Burr

2
VC ++ 2010 (w każdym razie beta 1) ma <stdint.h>i <cstdint>. Jeśli chodzi o obecny stan rzeczy - biblioteka VC ++ pochodzi z Dinkumware (nadal tak jest - stamtąd też został wzięty TR1), ale z tego, co pamiętam, czytałem na VCBlog, przechodzi dość znaczną refaktoryzację, aby skompilować się czysto /clr, współpracuje ze wszystkimi MSVC niestandardowe typy, takie jak __int64i tak dalej - dlatego nie jest to tak proste, jak po prostu pobranie go i umieszczenie w następnej wersji kompilatora.
Pavel Minaev

2
To doprowadziło mnie do poprawnej odpowiedzi, ale myślę, że powinieneś porównać do UINT64_MAX, a nie INT64_MAX. Użyłem SIZE_MAX == UINT64_MAX - prawdopodobnie to samo
Arno Duvenhage

15

Na początek to nie zadziała w systemie Windows. Długie i int są 32-bitowe, niezależnie od tego, czy kompilujesz dla okien 32-bitowych, czy 64-bitowych. Myślę, że sprawdzenie, czy rozmiar wskaźnika wynosi 8 bajtów, jest prawdopodobnie bardziej niezawodną trasą.


2
Niestety sizeof jest zabronione w dyrektywie #if (jeśli się nad tym zastanowić preprocesor nie jest w stanie tego stwierdzić)
EFraim

Tak, dlatego zostawiłem to, sugerując sprawdzenie rozmiaru wskaźnika zamiast używania sizeof - nie mogę wymyślić przenośnego sposobu, aby to zrobić z czubka głowy ...
mattnewport

3
Pytanie nie mówi (jeszcze), że należy to zrobić w czasie preprocesora. Wiele / większość kompilatorów z włączoną optymalizacją wykona porządną robotę, eliminując martwy kod, nawet jeśli „zostawisz to do czasu uruchomienia” za pomocą takiego testu sizeof(void*) == 8 ? Do64Bit() : Do32Bit();. Może to nadal pozostawić nieużywaną funkcję w pliku binarnym, ale wyrażenie jest prawdopodobnie skompilowane tylko do wywołania funkcji „właściwej”.
Steve Jessop

1
@onebyone, co rozwiązuje problem wywołań funkcji, ale co jeśli chcę zadeklarować zmienną innego typu w oparciu o platformę, musiałoby to być zrobione na preprocesorze, chyba że chcesz zadeklarować wiele zmiennych i użyć ich na podstawie instrukcji if ( który zostałby również zoptymalizowany, gdyby nie był używany, ale nie byłby zbyt przyjemny w kodzie)
Falaina

1
W takim razie masz rację, ciągłe wyrażanie warunku nie jest dobre. Jednak podejście Kirilla może zrobić, co chcesz:template<int> struct Thing; template<> struct Thing<4> { typedef uint32_t type; }; template<> struct Thing<8> { typedef uint64_t type; }; typedef Thing<sizeof(void*)>::type thingtype;
Steve Jessop

9

Możesz to zrobić:

#if __WORDSIZE == 64
char *size = "64bits";
#else
char *size = "32bits";
#endif

1
W wielu środowiskach programistycznych dla języków C i C na maszynach 64-bitowych zmienne „int” mają nadal 32 bity szerokości, ale długie liczby całkowite i wskaźniki mają szerokość 64 bity. Są one opisane jako mające model danych LP64. unix.org/version2/whatsnew/lp64_wp.html
Hermes,

6
Try this:
#ifdef _WIN64
// 64 bit code
#elif _WIN32
// 32 bit code
#else
   if(sizeof(void*)==4)

       // 32 bit code
   else 

       // 64 bit code   
#endif

7
Ten kod jest nieprawidłowy. W wersji 64-bitowej zdefiniowano zarówno _WIN32, jak i _WIN64. Jeśli to odwrócisz (najpierw sprawdź _WIN64), to oczywiście działa.
BertR,

4

„Kompilacja w wersji 64-bitowej” nie jest dobrze zdefiniowana w C ++.

C ++ ustawia tylko dolne limity dla rozmiarów, takich jak int, long i void *. Nie ma gwarancji, że int jest 64-bitowy, nawet po skompilowaniu dla platformy 64-bitowej. Model dopuszcza np. 23 bity intisizeof(int *) != sizeof(char *)

Istnieją różne modele programowania dla platform 64-bitowych.

Najlepszym rozwiązaniem jest test specyficzny dla platformy. Twój drugi najlepszy przenośne decyzja musi być bardziej szczegółowe w co jest 64-bitowy.


3

Twoje podejście nie było zbyt odległe, ale sprawdzasz tylko, czy longi czy intsą tej samej wielkości. Teoretycznie oba mogą mieć 64 bity, w takim przypadku twój test nie powiedzie się, zakładając, że oba mają 32 bity. Oto kontrola, która faktycznie sprawdza rozmiar samych typów, a nie ich względny rozmiar:

#if ((UINT_MAX) == 0xffffffffu)
    #define INT_IS32BIT
#else
    #define INT_IS64BIT
#endif
#if ((ULONG_MAX) == 0xfffffffful)
    #define LONG_IS32BIT
#else
    #define LONG_IS64BIT
#endif

W zasadzie możesz to zrobić dla każdego typu, dla którego masz zdefiniowane przez system makro z wartością maksymalną.

Zauważ, że standard wymaga long longco najmniej 64 bitów, nawet w systemach 32-bitowych.


Jedna rzecz, na którą należy zwrócić uwagę, aby zdefiniować UINT_MAX i ULONG_MAX, prawdopodobnie chcesz mieć #include <limits.h>gdzieś przed #iftestami.
Alexis Wilke

3

Ludzie już zasugerowali metody, które spróbują określić, czy program jest kompilowany w 32-bitlub 64-bit.

I chcę dodać, że możesz użyć funkcji c ++ 11, static_assertaby upewnić się, że architektura jest taka, jaką myślisz („odprężyć się”).

A więc w miejscu, w którym definiujesz makra:

#if ...
# define IS32BIT
  static_assert(sizeof(void *) == 4, "Error: The Arch is not what I think it is")
#elif ...
# define IS64BIT
  static_assert(sizeof(void *) == 8, "Error: The Arch is not what I think it is")
#else
# error "Cannot determine the Arch"
#endif

static_assert(sizeof(void*) * CHAR_BIT == 32)jest bardziej wyrazista i poprawna technicznie (chociaż nie znam żadnej architektury, w której bajty mają inną liczbę bitów niż 8)
Xeverous

2

Poniższy kod działa dobrze w większości obecnych środowisk:

  #if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) &&     !defined(__ILP32__) ) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__)
    #define IS64BIT 1
 #else
    #define IS32BIT 1
#endif

3
Zauważ, że _WIN64wymaga to już uwzględnienia <windows.h>. Visual C ++, to lepiej użyć wbudowanego Definiuje kompilatora: _M_IX86, _M_X64, _M_ARM, _M_ARM64, itp
Chuck Walbourn

Dla PowerPC, wierzę, trzeba sprawdzić __ppc64__, __powerpc64__i _ARCH_PPC64. To łapie także AIX i inne platformy.
jww

1

Jeśli możesz używać konfiguracji projektu we wszystkich swoich środowiskach, ułatwiłoby to zdefiniowanie 64- i 32-bitowego symbolu. Więc miałbyś takie konfiguracje projektu:

32-bitowe debugowanie
32-bitowe wydanie
64-bitowe debugowanie
64-bitowe wydanie

EDYCJA: są to konfiguracje ogólne, a nie docelowe. Zadzwoń do nich, jak chcesz.

Jeśli nie możesz tego zrobić, podoba mi się pomysł Jareda.


Lub połącz te dwa elementy: automatycznie wykryj konfigurację na kompilatorach, o których wiesz, ale wróć do patrzenia na #define określone w projekcie / wierszu polecenia / cokolwiek na nierozpoznanych kompilatorach.
Steve Jessop

4
W jaki sposób twoje rozwiązanie specyficzne dla VisualStudio pomoże w rozwiązaniu problemu wieloplatformowego OP?
alex tingle

3
@Jon: Hmm. Z definicji NIE są one obsługiwane w żadnym środowisku wieloplatformowym . Chyba że jest to definicja wieloplatformowości według MS - działa na nowszych wersjach systemu Windows.
EFraim

1
@EFraim: Tak, możesz CELOWAĆ na 32- lub 64-bitowe, używając VS, ale nie o tym mówię. Ogólne konfiguracje projektów i nazwy, które im przypisuję, nie mają absolutnie nic wspólnego z platformą. Jeśli konfiguracje projektów są specyficzne dla VS, szkoda, ponieważ są bardzo przydatne.
Jon Seigel

1
Myślę, że to właściwa odpowiedź. Jest bardziej niezawodny niż próba automatycznego wykrywania rzeczy. Wszystkie IDE, jakie kiedykolwiek widziałem, obsługują tę funkcję w jakiejś formie i założę się, że te, których nigdy nie widziałem, również ją obsługują. Jeśli używasz make lub jam, możesz ustawić zmienne z wiersza poleceń po wywołaniu, w zwykły sposób.

1

Umieściłbym źródła 32-bitowe i 64-bitowe w różnych plikach, a następnie wybrałbym odpowiednie pliki źródłowe za pomocą systemu kompilacji.


2
Byłoby to podobne do sytuacji, w której system kompilacji dałby ci flagę, taką jak -DBUILD_64BIT. Często niektóre rzeczy są bardzo podobne do 32- i 64-bitowych, więc umieszczenie ich w tym samym pliku może być całkiem praktyczne.
Alexis Wilke

Utrzymywanie bliźniaczych plików źródłowych jest podatne na błędy. IMO nawet ogromny #if bit64 .. cały kod, dla 64-bitowego #else .. cały kod, dla 32-bitowego #endif jest lepszy niż to. (# if linia po linii jest moim zdaniem idealna)
brewmanz

1

Pożyczanie od contango jest doskonałą odpowiedzią powyżej i łącząc ją z « lepszych makr, lepszy Flags » z Fluent C ++, można zrobić:

// Macro for checking bitness (safer macros borrowed from 
// https://www.fluentcpp.com/2019/05/28/better-macros-better-flags/)
#define MYPROJ_IS_BITNESS( X ) MYPROJ_IS_BITNESS_PRIVATE_DEFINITION_##X()

// Bitness checks borrowed from https://stackoverflow.com/a/12338526/201787
#if _WIN64 || ( __GNUC__ && __x86_64__ )
#    define MYPROJ_IS_BITNESS_PRIVATE_DEFINITION_64() 1
#    define MYPROJ_IS_BITNESS_PRIVATE_DEFINITION_32() 0
#    define MYPROJ_IF_64_BIT_ELSE( x64, x86 ) (x64)
    static_assert( sizeof( void* ) == 8, "Pointer size is unexpected for this bitness" );
#elif _WIN32 || __GNUC__
#    define MYPROJ_IS_BITNESS_PRIVATE_DEFINITION_64() 0
#    define MYPROJ_IS_BITNESS_PRIVATE_DEFINITION_32() 1
#    define MYPROJ_IF_64_BIT_ELSE( x64, x86 ) (x86)
    static_assert( sizeof( void* ) == 4, "Pointer size is unexpected for this bitness" );
#else
#    error "Unknown bitness!"
#endif

Następnie możesz go używać tak:

#if MYPROJ_IS_BITNESS( 64 )
    DoMy64BitOperation()
#else
    DoMy32BitOperation()
#endif

Lub korzystając z dodatkowego makra, które dodałem:

MYPROJ_IF_64_BIT_ELSE( DoMy64BitOperation(), DoMy32BitOperation() );

0

Dodaję tę odpowiedź jako przypadek użycia i kompletny przykład sprawdzania środowiska uruchomieniowego opisanego w innej odpowiedzi .

Oto podejście, które obrałem, aby przekazać użytkownikowi końcowemu, czy program został skompilowany jako 64-bitowy, czy 32-bitowy (lub inny, jeśli o to chodzi):

version.h

#ifndef MY_VERSION
#define MY_VERSION

#include <string>

const std::string version = "0.09";
const std::string arch = (std::to_string(sizeof(void*) * 8) + "-bit");

#endif

test.cc

#include <iostream>
#include "version.h"

int main()
{
    std::cerr << "My App v" << version << " [" << arch << "]" << std::endl;
}

Kompiluj i testuj

g++ -g test.cc
./a.out
My App v0.09 [64-bit]
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.