Deve haver asserções nas versões do release


20

O comportamento padrão do assertC ++ é não fazer nada nas compilações de versão. Presumo que isso seja feito por razões de desempenho e talvez para impedir que os usuários vejam mensagens de erro desagradáveis.

No entanto, eu argumentaria que as situações em que um assertteria disparado mas foi desativado são ainda mais problemáticas, porque o aplicativo provavelmente trava de maneira ainda pior na linha porque alguns invariantes foram quebrados.

Além disso, o argumento de desempenho para mim conta apenas quando é um problema mensurável. Muitos asserts no meu código não são muito mais complexos do que

assert(ptr != nullptr);

o que terá um pequeno impacto na maioria dos códigos.

Isso me leva à pergunta: As asserções (significando o conceito, não a implementação específica) devem ser ativas nas compilações de versão? Por que não)?

Observe que esta pergunta não é sobre como habilitar afirmações em compilações de versão (como #undef _NDEBUGou usando uma implementação de declaração definida). Além disso, não se trata de ativar declarações no código de biblioteca padrão / de terceiros, mas no código controlado por mim.


Conheço um participante global no mercado de assistência médica que instalou uma versão de depuração do sistema de informações hospitalares nos sites dos clientes. Eles tinham um contrato especial com a Microsoft para instalar as bibliotecas de depuração C ++ também. Bem, a qualidade do seu produto era ...
Bernhard Hiller

2
Há um velho ditado que diz: "remover afirmações é um pouco como vestir um colete salva-vidas para praticar no porto, mas deixar os coletes para trás quando seu navio partir para o mar aberto" ( wiki.c2.com/?AssertionsAsDefensiveProgramming ) . Pessoalmente, mantenho-os ativados nas versões de lançamento por padrão. Infelizmente, essa prática não é muito comum no mundo C ++, mas pelo menos o renomado veterano em C ++ James Kanze sempre argumentava a favor de manter as asserções nas versões: stackoverflow.com/a/12162195/3313064
Christian Hackl

Respostas:


20

O clássico asserté uma ferramenta da antiga biblioteca C padrão, não do C ++. Ainda está disponível em C ++, pelo menos por razões de compatibilidade com versões anteriores.

Não tenho uma linha do tempo precisa das bibliotecas padrão C disponíveis, mas tenho certeza de que assertestava disponível logo após o momento em que o K&R C entrou no ar (por volta de 1978). No C clássico, para escrever programas robustos, a adição de testes de ponteiro NULL e verificação de limites de matriz precisa ser feita com muito mais frequência do que no C ++. O grosso dos testes de ponteiro NULL pode ser evitado usando referências e / ou ponteiros inteligentes em vez de ponteiros, e usando a std::vectorverificação de limites de matriz geralmente é desnecessária. Além disso, o desempenho atingido em 1980 foi definitivamente muito mais importante do que hoje. Portanto, acho que é muito provável que a razão pela qual "assert" tenha sido projetada para estar ativa apenas em compilações de depuração por padrão.

Além disso, para tratamento real de erros no código de produção, uma função que apenas testa alguma condição ou invariável e trava o programa se a condição não for atendida, na maioria dos casos não é suficientemente flexível. Para depuração, provavelmente isso é bom, já que quem executa o programa e observa o erro geralmente possui um depurador para analisar o que acontece. Para o código de produção, no entanto, uma solução sensata precisa ser uma função ou mecanismo que

  • testa alguma condição (e interrompe a execução no escopo em que a condição falha)

  • fornece uma mensagem de erro clara, caso a condição não se mantenha

  • permite que o escopo externo receba a mensagem de erro e a envie para um canal de comunicação específico. Esse canal pode ser algo como stderr, um arquivo de log padrão, uma caixa de mensagem em um programa da GUI, um retorno de chamada geral de tratamento de erros, um canal de erro ativado pela rede ou o que melhor se adequar ao software específico.

  • permite que o escopo externo em uma base por caso decida se o programa deve terminar normalmente ou se deve continuar.

(Obviamente, também existem situações em que terminar o programa imediatamente no caso de uma condição não realizada é a única opção sensata, mas, nesses casos, deve ocorrer também em uma versão de compilação, e não apenas na versão de depuração).

Como o clássico assertnão fornece esses recursos, não é um bom ajuste para uma versão compilada, assumindo que a versão compilada é o que é implementado na produção.

Agora você pode perguntar por que não existe tal função ou mecanismo na lib padrão C, que fornece esse tipo de flexibilidade. Na verdade, em C ++, existe um mecanismo padrão que possui todos esses recursos (e mais), e você sabe disso: é chamado de exceções .

Em C, no entanto, é difícil implementar um bom mecanismo padrão de uso geral para o tratamento de erros com todos os recursos mencionados devido à falta de exceções como parte da linguagem de programação. Portanto, a maioria dos programas C possui seus próprios mecanismos de tratamento de erros com códigos de retorno, ou "goto" s, ou "long jumps", ou uma mistura disso. Geralmente, são soluções pragmáticas que se ajustam ao tipo específico de programa, mas não são "de uso geral o suficiente" para se encaixarem na biblioteca padrão C.


Ah, esqueci totalmente da herança C (afinal de contas #include <cassert>). Isso faz totalmente sentido agora.
Ninguém

1
Discordo completamente de toda essa resposta. Lançar uma exceção quando seu aplicativo descobrir que seu código-fonte estava com bugs é o último recurso em linguagens como Java, que não podem se sair melhor, mas em C ++, de todo modo, encerram o programa imediatamente em tal situação. Você não quer pilha desenrolamento para causar quaisquer outras operações a serem executadas nesse ponto. Um programa com erros pode gravar dados possivelmente corrompidos em arquivos, bancos de dados ou soquetes de rede. Basta travar o mais rápido possível (e possivelmente ter o processo reiniciado por um mecanismo externo).
Christian Hackl

1
@ChristianHackl: Eu concordo que há situações em que o encerramento de um programa é a única opção viável, mas também deve ocorrer no modo de lançamento, e também asserté a ferramenta errada para isso (lançar uma exceção também pode ser a reação errada) ) No entanto, tenho certeza de que, na realidade, asserttambém foi usado para muitos testes em C, onde no C ++ se usaria exceções hoje.
Doc Brown

15

Se você deseja que as declarações tenham sido ativadas em uma liberação, solicitou que as declarações fizessem o trabalho errado.

O objetivo das afirmações é que elas não estão ativadas em uma liberação. Isso permite testar invariantes durante o desenvolvimento com código que, de outra forma, teria que ser código de andaime. Código que deve ser removido antes do lançamento.

Se você tem algo que acha que deve ser testado mesmo durante o lançamento, escreva um código que o teste. A If throwconstrução funciona muito bem. Se você deseja dizer algo diferente do que os outros lançamentos, use uma exceção descritiva que diz o que você deseja dizer.

Não é que você não possa mudar a maneira como usa declarações. É que isso não lhe proporciona nada de útil, contraria as expectativas e deixa você sem uma maneira limpa de fazer o que as declarações deveriam fazer. Adicione testes que estão inativos em uma liberação.

Não estou falando de uma implementação específica de assert, mas do conceito de asserção. Não quero abusar da afirmação ou confundir os leitores. Eu quis perguntar por que é assim em primeiro lugar. Por que não há release_assert adicional? Não há necessidade disso? Qual é a lógica por trás da afirmação de ser desativada na liberação? - Ninguém

Por que não relase_assert? Francamente, porque as afirmações não são boas o suficiente para a produção. Sim, há uma necessidade, mas nada preenche bem essa necessidade. Ah, claro que você pode criar o seu próprio. Mecanicamente, sua função throwIf precisa apenas de um bool e uma exceção para lançar. E isso pode atender às suas necessidades. Mas você está realmente limitando o design. É por isso que não me surpreende que não exista um sistema assertivo como exceção na sua biblioteca de idiomas. Certamente não é que você não possa fazer isso. Outros têm . Mas lidar com o caso em que as coisas dão errado é 80% do trabalho para a maioria dos programas. E até agora ninguém nos mostrou um bom tamanho único para todas as soluções. Lidar eficazmente com esses casos pode ser complicado. Se tivéssemos um sistema release_assert enlatado que não atendesse às nossas necessidades, acho que teria feito mais mal do que bem. Você está pedindo uma boa abstração, o que significaria que você não precisaria pensar nesse problema. Também quero um, mas parece que ainda não chegamos.

Por que as declarações são desativadas na versão? As declarações foram criadas no auge da idade do código do andaime. Código que tivemos que remover porque sabia que não o desejávamos na produção, mas sabíamos que queríamos rodar no desenvolvimento para nos ajudar a encontrar bugs. As declarações foram uma alternativa mais limpa ao if (DEBUG)padrão que nos deixou deixar o código, mas desativá-lo. Isso foi antes do teste de unidade decolar como a principal maneira de separar o código de teste do código de produção. As declarações ainda são usadas hoje, mesmo por testadores de unidade especializados, tanto para esclarecer as expectativas quanto para cobrir casos em que eles ainda se saem melhor do que os testes de unidade.

Por que não deixar apenas o código de depuração em produção? Como o código de produção precisa não envergonhar a empresa, não formatar o disco rígido, não corromper o banco de dados e não enviar e-mails ameaçadores ao presidente. Em suma, é bom poder escrever código de depuração em um local seguro, onde você não precisa se preocupar com isso.


2
Acho que minha pergunta é: Por que esse código foi removido antes do lançamento? As verificações não são muito prejudiciais ao desempenho e, se falharem, há definitivamente um problema sobre o qual eu prefiro uma mensagem de erro mais direta. Que vantagem me oferece uma exceção em vez de uma declaração? Eu provavelmente não gostaria que um código não relacionado pudesse catch(...).
Ninguém

2
@ Ninguém A maior vantagem de não usar afirmações para necessidades não afirmativas não é confundir seus leitores. Se você não deseja que o código seja desativado, não use expressões que sinalizem que ele será. É tão ruim quanto usar if (DEBUG)para controlar algo diferente de código de depuração. A micro otimização pode não significar nada no seu caso. Mas o idioma não deve ser subvertido apenas porque você não precisa dele.
precisa saber é o seguinte

Eu acho que não deixei minha intenção clara o suficiente. Não estou falando de uma implementação específica, assertmas do conceito de uma afirmação. Não quero abusar assertou confundir os leitores. Eu quis perguntar por que é assim em primeiro lugar. Por que não há mais release_assert? Não há necessidade disso? Qual é a lógica por trás da afirmação de ser desativada na liberação?
Ninguém

2
You're asking for a good abstraction.Eu não tenho certeza sobre isso. Quero principalmente lidar com questões em que não há recuperação e isso nunca deve acontecer. Leve isso junto Because production code needs to [...] not format the hard drive [...]e eu diria que, nos casos em que os invariantes são quebrados, eu faria uma declaração sobre o UB a qualquer momento.
Ninguém

1
@Novidades "onde não há recuperação" podem ser resolvidas lançando exceções e não as capturando. Você pode observar que eles nunca devem ocorrer no texto da mensagem da exceção.
candied_orange

4

As asserções são uma ferramenta de depuração , não uma técnica de programação defensiva. Se você deseja executar a validação em todas as circunstâncias, execute a validação escrevendo uma condicional - ou crie sua própria macro para reduzir o padrão.


4

asserté uma forma de documentação, como comentários. Como os comentários, você normalmente não os envia aos clientes - eles não pertencem ao código de liberação.

Mas o problema com os comentários é que eles podem ficar desatualizados e, no entanto, são deixados. É por isso que as afirmações são boas - elas são verificadas no modo de depuração. Quando a afirmação se torna obsoleta, você a descobre rapidamente e ainda sabe como consertar a afirmação. Esse comentário que ficou desatualizado há 3 anos? Alguém sabe.


1
Portanto, abortar uma invariante com falha antes que algo ocorra acontecer não é um caso de uso válido?
Ninguém

3

Se você não deseja que uma "declaração" seja desativada, sinta-se à vontade para escrever uma função simples que tenha um efeito semelhante:

void fail_if(bool b) {if(!b) std::abort();}

Ou seja, asserté para testes onde você fazer deseja que eles vão embora no produto enviado. Se você deseja que esse teste faça parte do comportamento definido do programa, asserté a ferramenta errada.


1
Estou bem ciente disso. A questão está mais na linha de por que o padrão é o que é.
Ninguém

3

Não faz sentido argumentar o que a afirmação deve fazer, ela faz o que faz. Se você quiser algo diferente, escreva sua própria função. Por exemplo, tenho Assert que para no depurador ou não faz nada, tenho AssertFatal que irá travar o aplicativo, tenho funções booleanas Asserted e AssertionFailed que afirmam e retornam o resultado para que eu possa afirmar e lidar com a situação.

Para qualquer problema inesperado, você precisa decidir qual é a melhor maneira para o desenvolvedor e o usuário lidar com isso.


Não era minha intenção discutir sobre o que a afirmação deveria fazer. Minha motivação para esta pergunta foi encontrar o raciocínio por trás das afirmações que não estão no build do release, porque estou prestes a escrever o meu e quero reunir informações sobre casos de uso, expectativas e armadilhas.
Ninguém

Hum, não verify(cond)seria a maneira normal de afirmar e retornar o resultado?
Deduplicator

1
Estou codificando em Ruby, não em C, mas discordo da maioria das respostas acima, pois interromper o programa com a impressão de um backtrace funciona bem para mim como uma técnica de programação defensiva ....... meu comentário não se encaixava no tamanho máximo do comentário em caracteres, então escrevi outra resposta abaixo.
Nakilon

2

Como outros apontaram, esse asserté o seu último bastião de defesa contra erros de programadores que nunca devem acontecer. São verificações de sanidade que, esperançosamente, não devem falhar esquerda e direita no momento em que você envia.

Ele também foi projetado para ser omitido nas versões de versões estáveis, por qualquer motivo que os desenvolvedores possam achar úteis: estética, desempenho, o que quiserem. É parte do que separa uma compilação de depuração de uma compilação de lançamento e, por definição, uma compilação de lançamento é desprovida de tais asserções. Portanto, há uma subversão do design, se você deseja liberar o analógico "release build com assertions in place", que seria uma tentativa de um release build com uma _DEBUGdefinição de pré - processador e sem NDEBUGdefinição; não é realmente uma versão mais compilada.

O design se estende até a biblioteca padrão. Como um exemplo muito básico entre inúmeras, muitas implementações de uma verificação de sanidade std::vector::operator[]irão assertgarantir que você não esteja verificando o vetor fora dos limites. E a biblioteca padrão começará a ter um desempenho muito pior se você habilitar essas verificações em uma versão. Uma referência de vectorusooperator[]e um preenchedor com essas afirmações incluídas em uma matriz dinâmica antiga simples geralmente mostra que a matriz dinâmica é consideravelmente mais rápida até você desabilitar essas verificações, de modo que elas afetam o desempenho de maneiras distantes e distantes. Uma verificação de ponteiro nulo aqui e uma verificação fora dos limites podem realmente se tornar uma despesa enorme se essas verificações estiverem sendo aplicadas milhões de vezes em todos os quadros em loops críticos que precedem o código, tão simples quanto desreferenciar um ponteiro inteligente ou acessar uma matriz.

Portanto, é mais provável que você deseje uma ferramenta diferente para o trabalho e uma que não seja projetada para ser omitida nas compilações de versões, se você quiser que compilações de versões que executam tais verificações de sanidade em áreas-chave. O mais útil que eu pessoalmente acho é o log. Nesse caso, quando um usuário relata um bug, as coisas ficam muito mais fáceis se eles anexam um log e a última linha do log me dá uma grande pista de onde o bug ocorreu e qual pode ser. Em seguida, ao reproduzir suas etapas em uma compilação de depuração, da mesma forma, eu poderia ter uma falha de afirmação, e essa falha de afirmação ainda me dá grandes pistas para otimizar meu tempo. No entanto, como o registro é relativamente caro, não o uso para aplicar verificações de sanidade de nível extremamente baixo, como garantir que uma matriz não seja acessada fora dos limites em uma estrutura de dados genérica.

No entanto, finalmente, e um pouco de acordo com você, eu pude ver um caso razoável em que você pode realmente entregar aos testadores algo semelhante a uma compilação de depuração durante o teste alfa, por exemplo, com um pequeno grupo de testadores alfa que, digamos, assinaram um NDA . Lá, ele pode otimizar o teste alfa se você entregar a seus testadores algo diferente de uma versão completa com algumas informações de depuração anexadas, além de alguns recursos de depuração / desenvolvimento, como testes que eles podem executar e uma saída mais detalhada enquanto executam o software. Eu já vi pelo menos algumas grandes empresas de jogos fazendo coisas assim para o alfa. Mas isso é algo como testes alfa ou internos, nos quais você está realmente tentando dar aos testadores algo diferente de uma versão compilada. Se você está realmente tentando enviar uma versão compilada, por definição, não deve ter_DEBUG definido ou isso realmente está confundindo a diferença entre uma compilação "debug" e "release".

Por que esse código foi removido antes do lançamento? As verificações não são muito prejudiciais ao desempenho e, se falharem, há definitivamente um problema sobre o qual eu prefiro uma mensagem de erro mais direta.

Como indicado acima, as verificações não são necessariamente triviais do ponto de vista de desempenho. Muitos são provavelmente triviais, mas, novamente, até a biblioteca padrão os usa e isso pode afetar o desempenho de maneiras inaceitáveis ​​para muitas pessoas em muitos casos, se, digamos, a passagem de acesso aleatório std::vectordemorar 4 vezes mais do que deveria ser uma versão otimizada do build por causa de sua verificação de limites que nunca deveria falhar.

Em uma equipe anterior, na verdade, tivemos que fazer com que nossa biblioteca de matrizes e vetores excluísse algumas afirmações em certos caminhos críticos, apenas para acelerar a compilação de depuração, porque essas afirmações estavam diminuindo as operações matemáticas em uma ordem de grandeza até o ponto em que estavam. começando a exigir que esperemos 15 minutos antes que pudéssemos rastrear o código de interesse. Meus colegas queriam apenas remover oassertsdiretamente porque descobriram que apenas fazer isso fazia uma diferença enorme. Em vez disso, decidimos apenas fazer com que os caminhos críticos de depuração os evitassem. Quando fizemos esses caminhos críticos usarem os dados vetoriais / matriz diretamente, sem passar pela verificação de limites, o tempo necessário para executar a operação completa (que incluía mais do que apenas matemática vetorial / matriz) reduzido de minutos para segundos. Portanto, esse é um caso extremo, mas definitivamente as afirmações nem sempre são insignificantes do ponto de vista de desempenho, nem mesmo próximas.

Mas também é assim que assertssão projetados. Se eles não tiveram um impacto de desempenho tão grande em todos os aspectos, talvez eu seja a favor se eles foram projetados como mais do que um recurso de compilação de depuração ou podemos usar o vector::atque inclui a verificação de limites, mesmo em versões de lançamento e lançamentos fora dos limites acesso, por exemplo (ainda com um enorme impacto no desempenho). Mas atualmente acho o design deles muito mais útil, dado o enorme impacto no desempenho dos meus casos, como um recurso somente de depuração, que é omitido quando NDEBUGdefinido. Para os casos com os quais trabalhei, pelo menos, faz uma diferença enorme a criação de uma versão excluir as verificações de sanidade que nunca deveriam realmente falhar em primeiro lugar.

vector::at vs. vector::operator[]

Eu acho que a distinção desses dois métodos está no centro disso e da alternativa: exceções. vector::operator[]implementações normalmente assertpara garantir que o acesso fora dos limites acione um erro facilmente reproduzível ao tentar acessar um vetor fora dos limites. Mas os implementadores da biblioteca fazem isso com o pressuposto de que não custará um centavo em uma compilação de versão otimizada.

Enquanto isso, vector::até fornecido o que sempre verifica e lança fora dos limites, mesmo nas compilações de versão, mas tem uma penalidade de desempenho até o ponto em que geralmente vejo muito mais código usando do vector::operator[]que vector::at. Muito do design do C ++ faz eco à idéia de "pagar pelo que você usa / precisa", e muitas pessoas são a favor operator[], o que nem se importa com a verificação de limites nas versões de lançamento, com base na noção de que eles não não precisa verificar os limites em suas compilações de versão otimizadas. De repente, se as asserções fossem ativadas nas compilações de versão, o desempenho dessas duas seria idêntico e o uso do vetor sempre acabaria sendo mais lento que um array dinâmico. Portanto, grande parte do design e dos benefícios das asserções é baseada na idéia de que elas se tornam livres em uma versão compilada.

release_assert

Isso é interessante depois de descobrir essas intenções. Naturalmente, os casos de uso de todos seriam diferentes, mas acho que encontraria algum uso para um release_assertque faça a verificação e trava o software, mostrando um número de linha e uma mensagem de erro, mesmo nas versões lançadas.

Seria para alguns casos obscuros no meu caso que eu não quero que o software se recupere normalmente, como faria se uma exceção fosse lançada. Eu gostaria que ele falhasse mesmo na versão nesses casos, para que o usuário possa receber um número de linha para relatar quando o software encontrou algo que nunca deveria acontecer, ainda no campo das verificações de sanidade quanto a erros do programador, não erros externos de entrada como exceções, mas barato o suficiente para ser feito sem se preocupar com seu custo no lançamento.

Na verdade, existem alguns casos em que eu considerava uma falha grave com um número de linha e uma mensagem de erro preferíveis à recuperação normal de uma exceção lançada que pode ser barata o suficiente para manter em uma versão. E há alguns casos em que é impossível recuperar de uma exceção, como um erro encontrado ao tentar recuperar de uma existente. Lá, eu encontraria um ajuste perfeito para, release_assert(!"This should never, ever happen! The software failed to fail!");naturalmente, que seria muito barato, já que a verificação seria realizada dentro de um caminho excepcional em primeiro lugar e não custaria nada nos caminhos normais de execução.


Minha intenção não era ativar declarações no código de terceiros, consulte a edição de esclarecimento. A citação do meu comentário omite o contexto da minha pergunta: Additionally, the performance argument for me only counts when it is a measurable problem.Portanto, a idéia é decidir, caso a caso, quando tirar uma afirmação do release.
Ninguém

Um dos problemas com o último comentário é que a biblioteca padrão usa o mesmo assertque se baseia nos mesmos _DEBUG/NDEBUGpadrões de pré-processador que o que você usaria ao usar a assertmacro. Portanto, não há como habilitar seletivamente asserções apenas para um trecho de código sem habilitá-lo também para a lib padrão, por exemplo, supondo que ele use a lib padrão.

Há uma verificação do iterador STL que você pode desativar separadamente, o que desativa algumas das asserções, mas não todas.

É basicamente uma ferramenta grossa, não granular, e sempre com a intenção de desabilitar o release, para que seus colegas de equipe possam usá-lo em áreas críticas contra a suposição de que isso não afetará o desempenho do release, a biblioteca padrão o utiliza. em áreas críticas, outras bibliotecas de terceiros a usam dessa maneira etc. E quando você deseja algo, pode desativar / ativar mais especificamente um trecho de código e não o outro, voltamos a analisar outra coisa que não assert.

vector::operator[]e vector::at, por exemplo, teria praticamente o mesmo desempenho se as asserções estivessem ativadas nas compilações de versão, e não haveria sentido em usar operator[]mais do ponto de vista de desempenho, pois, de qualquer maneira, ele faria a verificação de limites.

2

Estou codificando em Ruby, não em C / C ++ e, portanto, não falarei sobre diferenças entre declarações e exceções, mas gostaria de falar sobre isso como algo que interrompe o tempo de execução . Eu discordo da maioria das respostas acima, pois interromper o programa com a impressão de um backtrace funciona bem para mim como uma técnica de programação defensiva.
Se existe uma maneira de afirmar a rotina (absolutamente, independentemente de como é sintaticamente escrita e se a palavra "afirmar" é usada ou existe na linguagem de programação ou no dsl), isso significa que algum trabalho deve ser feito e o produto imediatamente volta do "liberado, pronto para o uso" para "precisa de correção" - agora reescreva-o para tratamento de exceções reais ou corrija um bug que causou a exibição de dados incorretos.

Quero dizer, afirmar não é algo com o qual você deve conviver e ligar com frequência - é um sinal de parada que indica que você deve fazer algo para que isso nunca aconteça novamente. E dizer que "release build não deve ter afirmações" é como dizer "release build não deve ter bugs" - cara, é quase impossível, lidar com isso.
Ou pense neles como "falhas de unittests feitas e executadas pelo usuário final". Você não pode prever tudo o que o usuário fará com o seu programa, mas se algo muito sério der errado, ele deverá parar - isso é semelhante à maneira como você constrói pipelines de construção - você interrompe o processo e não publica, não é? ? A afirmação força o usuário a parar, relatar e aguardar sua ajuda.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.