Por que usar funções de início e término de não membros no C ++ 11?

197

Todo contêiner padrão possui um método begine endpara retornar iteradores para esse contêiner. No entanto, o C ++ 11 aparentemente introduziu funções gratuitas chamadas std::begine std::endque chamam as funções de membro begine end. Então, ao invés de escrever

auto i = v.begin();
auto e = v.end();

você escreveria

auto i = std::begin(v);
auto e = std::end(v);

Em sua palestra, Writing Modern C ++ , Herb Sutter diz que você deve sempre usar as funções livres agora quando quiser o iterador inicial ou final de um contêiner. No entanto, ele não entra em detalhes sobre o motivo pelo qual você gostaria. Observando o código, você economiza todos os caracteres. Portanto, no que diz respeito aos contêineres padrão, as funções livres parecem ser completamente inúteis. Herb Sutter indicou que havia benefícios para recipientes fora do padrão, mas, novamente, ele não entrou em detalhes.

Portanto, a questão é o que exatamente as versões de funções livres std::begine std::endalém de chamar suas versões correspondentes de funções-membro, e por que você deseja usá-las?

Jonathan M Davis
fonte
29
É um caractere a menos, salve esses pontos para seus filhos: xkcd.com/297
HostileFork diz que não confia em SE
De alguma forma, eu odeio usá-las porque tenho que repetir std::o tempo todo.
Michael Chourdakis 14/01

Respostas:

162

Como você chama .begin()e .end()em uma matriz C?

As funções livres permitem programação mais genérica porque podem ser adicionadas posteriormente, em uma estrutura de dados que você não pode alterar.

Matthieu M.
fonte
7
@ JonathanMDavis: você pode obter as endmatrizes declaradas estaticamente ( int foo[5]) usando truques de programação de modelos. Depois de decair para um ponteiro, é claro que você está sem sorte.
Matthieu M.
33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh,
6
@ JonathanMDavis: Como os outros indicaram, é certamente possível obter begine obter enduma matriz C, desde que você ainda não a tenha decaído para um ponteiro - @Huw explica. Quanto ao motivo pelo qual você desejaria: imagine que você refatorou o código que estava usando uma matriz para usar um vetor (ou vice-versa, por qualquer motivo). Se você estiver usando begine end, e talvez algum tipo inteligente de digitação, o código de implementação não precisará ser alterado (exceto talvez alguns dos typedefs).
Karl Knechtel
31
@ JonathanMDavis: Arrays não são ponteiros. E para todos: para acabar com essa confusão sempre importante, pare de se referir a (alguns) ponteiros como "matrizes deterioradas". Não existe tal terminologia no idioma e realmente não há utilidade para isso. Ponteiros são ponteiros, matrizes são matrizes. As matrizes podem ser convertidas em um ponteiro para o primeiro elemento implicitamente, mas ainda é apenas um ponteiro antigo comum, sem distinção entre outras. É claro que você não pode obter o "fim" de um ponteiro, caso encerrado.
GManNickG 29/09
5
Bem, além das matrizes, há um grande número de APIs que expõem aspectos do contêiner. Obviamente, você não pode modificar uma API de terceiros, mas pode escrever facilmente essas funções de início / fim independentes.
edA-qa mort-ora-y
35

Considere o caso quando você tiver uma biblioteca que contenha classe:

class SpecialArray;

tem 2 métodos:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

para iterar sobre os valores que você precisa herdar dessa classe e definir begin()e end()métodos para casos em que

auto i = v.begin();
auto e = v.end();

Mas se você sempre usa

auto i = begin(v);
auto e = end(v);

você consegue fazer isso:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}

onde SpecialArrayIteratoré algo como:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};

agora ie epode ser usado legalmente para iteração e acesso aos valores do SpecialArray

GreenScape
fonte
8
Isso não deve incluir as template<>linhas. Você está declarando uma nova sobrecarga de função, não especializando um modelo.
David Stone
33

O uso das funções begine endfree adiciona uma camada de indireção. Geralmente isso é feito para permitir mais flexibilidade.

Neste caso, posso pensar em alguns usos.

O uso mais óbvio é para matrizes C (não ponteiros c).

Outra é quando se tenta usar um algoritmo padrão em um contêiner não conforme (ou seja, falta um .begin()método ao contêiner ). Supondo que você não possa consertar apenas o contêiner, a próxima melhor opção é sobrecarregar a beginfunção. Herb sugere que você sempre use a beginfunção para promover uniformidade e consistência no seu código. Em vez de ter que lembrar quais contêineres suportam o método begine quais precisam de função begin.

Como um aparte, rev a próxima C ++ deve copiar D's notação pseudo-membro . Se a.foo(b,c,d)não estiver definido, ele tenta foo(a,b,c,d). É apenas um pouco de açúcar sintático para ajudar-nos humanos pobres que preferem a disciplina do que a ordenação verbal.

deft_code
fonte
5
A notação de pseudo-membro se parece com os métodos de extensão C # /. Net . Eles são úteis para várias situações, embora - como todos os recursos - possam ser propensos a "abuso".
Gareth Wilson
5
A notação de pseudo-membro é um benefício para a codificação com o Intellisense; apertando "a". mostra verbos relevantes, liberando o poder do cérebro das listas de memorização e ajudando a descobrir funções relevantes da API podem ajudar a impedir a duplicação da funcionalidade, sem ter que calçar as funções de não membros nas classes.
Matt Curtis
Existem propostas para inserir isso no C ++, que usam o termo Sintaxe de chamada de função unificada (UFCS).
Underscore_d
17

Para responder à sua pergunta, as funções livres begin () e end () por padrão não fazem mais do que chamar as funções de membro .begin () e .end () do contêiner. De <iterator>, incluído automaticamente quando você usa qualquer um dos contêineres padrão <vector>, como <list>, etc., você obtém:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 

A segunda parte da sua pergunta é por que preferir as funções livres se tudo o que eles fazem é chamar as funções-membro de qualquer maneira. Isso realmente depende de que tipo de objeto vestá no seu código de exemplo. Se o tipo de v for um tipo de contêiner padrão, vector<T> v;não importa se você usa as funções free ou member, elas fazem a mesma coisa. Se seu objeto vfor mais genérico, como no código a seguir:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Em seguida, o uso das funções de membro quebra seu código para matrizes T = C, seqüências de caracteres C, enumerações etc. Ao usar as funções de não membro, você anuncia uma interface mais genérica que as pessoas podem facilmente estender. Usando a interface de função livre:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

O código agora funciona com matrizes T = C e seqüências de caracteres C. Agora, escreva uma pequena quantidade de código do adaptador:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }

Também podemos fazer com que seu código seja compatível com enumerações iteráveis. Eu acho que o ponto principal de Herb é que o uso das funções gratuitas é tão fácil quanto o uso das funções membro, e isso dá ao seu código compatibilidade retroativa com tipos de sequência C e compatibilidade direta com tipos de sequência não-stl (e futuros-stl!), com baixo custo para outros desenvolvedores.

Nate
fonte
Bons exemplos. Eu não usaria um enumou qualquer outro tipo fundamental por referência; eles serão mais baratos de copiar do que indiretos.
underscore_d
6

Um benefício std::begine std::endé que eles servem como pontos de extensão para implementar a interface padrão para classes externas.

Se você deseja usar a CustomContainerclasse com função for loop ou template baseada em intervalo, que espera .begin()e .end()métodos, obviamente precisará implementar esses métodos.

Se a classe fornecer esses métodos, isso não será um problema. Quando isso não acontecer, você precisará modificá-lo *.

Isso nem sempre é viável, por exemplo, ao usar uma biblioteca externa, especialmente a comercial e a de código fechado.

Nessas situações, std::begine std::endé útil, pois é possível fornecer API do iterador sem modificar a própria classe, mas sobrecarregando as funções livres.

Exemplo: suponha que você gostaria de implementar uma count_iffunção que utiliza um contêiner em vez de um par de iteradores. Esse código pode ficar assim:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}

Agora, para qualquer classe que você gostaria de usar com esse costume count_if, você só precisa adicionar duas funções livres, em vez de modificar essas classes.

Agora, o C ++ tem um mecanismo chamado Argl Dependent Lookup (ADL), que torna essa abordagem ainda mais flexível.

Em resumo, ADL significa que quando um compilador resolver uma função não qualificada (ou seja, função sem espaço para nome, como em beginvez de std::begin), ele também considerará funções declaradas nos espaços para nome dos seus argumentos. Por exemplo:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);

Neste caso, não importa que nomes qualificados são some_lib::begine some_lib::end - uma vez que CustomContainerestá em some_lib::muito, compilador usará essas sobrecargas no count_if.

Essa também é a razão de ter using std::begin;e using std::end;entrar count_if. Isso nos permite usar não qualificado begine end, portanto, permitindo ADL e permitindo que o compilador escolha std::begine std::endquando nenhuma outra alternativa for encontrada.

Podemos comer o cookie e ter o cookie - ou seja, ter uma maneira de fornecer uma implementação personalizada de begin/ endenquanto o compilador pode voltar aos padrões.

Algumas notas:

  • Pelo mesmo motivo, existem outras funções semelhantes: std::rbegin/ rend, std::sizee std::data.

  • Como outras respostas mencionam, as std::versões possuem sobrecargas para matrizes nuas. Isso é útil, mas é simplesmente um caso especial do que descrevi acima.

  • Usar std::begine amigos é uma boa idéia ao escrever o código do modelo, porque isso os torna mais genéricos. Para não-modelo, você também pode usar métodos, quando aplicável.

PS Estou ciente de que este post tem quase 7 anos. Me deparei com isso porque queria responder uma pergunta marcada como duplicada e descobri que nenhuma resposta aqui menciona ADL.

joe_chip
fonte
Boa resposta, particularmente explicando abertamente as AVDs, em vez de deixá-las para a imaginação, como todo mundo fazia - mesmo quando estavam mostrando isso em ação!
Underscore_d
5

Enquanto as funções de não membro não fornecem nenhum benefício para os contêineres padrão, usá-los impõe um estilo mais consistente e flexível. Se em algum momento você desejar estender uma classe de contêiner não-std existente, prefira definir sobrecargas das funções livres, em vez de alterar a definição da classe existente. Portanto, para contêineres não std, eles são muito úteis e sempre usar as funções livres torna seu código mais flexível, pois você pode substituir o contêiner std por um contêiner não std com mais facilidade e o tipo de contêiner subjacente é mais transparente ao seu código, pois suporta uma variedade muito maior de implementações de contêineres.

Mas é claro que isso sempre deve ser ponderado adequadamente e a abstração excessiva também não é boa. Embora o uso das funções livres não exija muita abstração, ele quebra a compatibilidade com o código C ++ 03, que nessa idade jovem do C ++ 11 ainda pode ser um problema para você.

Christian Rau
fonte
3
Em C ++ 03, você pode simplesmente usar boost::begin()/ end(), então não há nenhuma incompatibilidade verdadeira :)
Marc Mutz - mmutz
1
@ MarcMutz-mmutz Bem, aumentar a dependência nem sempre é uma opção (e é um exagero, se usado apenas para begin/end). Então, eu consideraria uma incompatibilidade com o C ++ 03 puro também. Mas, como dito anteriormente, é uma incompatibilidade bastante pequena (e cada vez menor), pois o C ++ 11 (pelo menos begin/endem particular) está cada vez mais adotando.
Christian Rau
0

Por fim, o benefício está no código que é generalizado, de modo que é independente do contêiner. Ele pode operar em um std::vector, um array ou um intervalo sem alterações no próprio código.

Além disso, os contêineres, mesmo os contêineres não pertencentes à propriedade, podem ser adaptados de forma que também possam ser usados ​​de forma independente por código, usando acessadores não baseados em intervalo de membros.

Veja aqui para mais detalhes.

Jonathan Mee
fonte