Tornando os loops for baseados em intervalos do C ++ 11 um pouco mais úteis

C ++ 11 é ótimo. Provavelmente um dos recursos mais bonitos (na minha opinião) é o chamado loop baseado em intervalo. Em vez de

for ( std::size_t i(0); i < range.size(); ++i ) { // do something to range[i]; } 

ou

for ( Range::iterator it(range.begin()); it != range.end(); ++it ) { // do something to *it; } 

ou simplificado com C ++ 11 auto:

for ( auto it(range.begin()); it != range.end(); ++it ) { // do something to *it; } 

podemos dizer o seguinte:

for ( auto& entry : range ) { // do something with entry. } 

Isso é muito expressivo: nós conversamos sobre a entrada, em vez da i ^ ésima posição no intervalo ou do iterador apontando para uma entrada. No entanto, essa sintaxe não tem a capacidade de ter subintervalos (por exemplo, ignorar a última entrada). Em uma série de pequenas estruturas / métodos auxiliares, quero adicionar essa funcionalidade de uma maneira limpa.

Isso é para minha motivação 😉 Agora, para o negócio real.

Nesta postagem, abordo as condições nas entradas de range. Basicamente, o código auxiliar deve executar o equivalente a

for ( auto& entry : range ) { if ( condition(entry) ) { // do something to entry. } } 

, mas sem esse nível de indentação.

Aqui está o código para ConditionalRange:

template <typename Range, typename Runnable> ConditionalRange<Range, Runnable> makeConditionalRange(Range& range, Runnable&& condition) { static_assert(std::is_same<decltype(condition(*std::declval<Range>().begin())), bool>::value, "Condition must return a boolean value."); return ConditionalRange<Range, Runnable>(range, std::forward<Runnable>(condition)); } template <typename Range, typename Runnable> struct ConditionalRange { public: friend ConditionalRange makeConditionalRange<>(Range&, Runnable&&); public: using iterator_type = ConditionalIterator<decltype(std::declval<Range>().begin()), Runnable>; iterator_type begin() const { auto b = range.begin(); while ( b != range.end() && !condition(*b) ) { ++b; } return ConditionalIterator<decltype(std::declval<Range>().begin()), Runnable>(b, range.end(), condition); } iterator_type end() const { return ConditionalIterator<decltype(std::declval<Range>().begin()), Runnable>(range.end(), range.end(), condition); } private: ConditionalRange(Range& range_, Runnable&& condition_) : range(range_), condition(std::forward<Runnable>(condition_)) { } private: Range& range; Runnable condition; }; 

Esta é a estrutura auxiliar para o iterador:

template <typename Iterator, typename Runnable> struct ConditionalIterator { private: Iterator iterator; Iterator end; const Runnable& condition; public: ConditionalIterator(Iterator b, Iterator e, const Runnable& r) : iterator(b), end(e), condition(r) { } auto operator*() -> decltype(*iterator) { return *iterator; } ConditionalIterator& operator++() { do { ++iterator; } while ( iterator != end && !condition(*iterator) ); return *this; } bool operator==(const ConditionalIterator& second) { return iterator == second.iterator; } bool operator!=(const ConditionalIterator& second) { return !(*this == second); } }; 

O uso pretendido é:

std::vector<int> ns{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; for ( const auto& n : makeConditionalRange(ns, [](int a) { return a % 2 == 0; }) ) { std::cout << n << " "; } std::cout << "\n"; 

Demo: clique aqui!

O que são os pontos fracos da minha abordagem? Com que tipo de argumentos ele falha, embora não devesse no mundo perfeito?

Comentários

  • Não vejo fraquezas, mas eu acho que você deve considerar renomear makeConditionalRange com algo que se concentre na aparência do código do cliente, não em " criar um intervalo condicional ". Por isso, eu significa que o código do cliente deve ter a seguinte aparência: for(const auto& n: iterate_if(ns, [](int a) { return a % 2 == 0; }) )
  • Então, a intenção aqui difere significativamente de um Boost filter_iterator ?
  • @JerryCoffin Não, não ' t. Simplesmente não tenho ' olhei para Boost 😉
  • O teste mais comum para iteradores operator!= otimiza isso em operator==
  • @stefan: Para ser franco, ' é bobo. É como dizer que não ' t costumo comprar um carro dos fabricantes padrão porque eu mesmo quero construir um que escorra algas marinhas para garantir que não polua. O problema é que seu conjunto de bibliotecas será infinitamente mais problemático do que otimizado, porque você só tem um par de olhos olhando para elas. O Boost tem milhares de pessoas verificando e validando a correção do código e fornecendo feedback para garantir que os idiomas corretos sejam usados corretamente. Leia: stackoverflow.com/a/149305/14065

Resposta

Você pode usar colchetes para retornar de suas funções. Dessa forma, você “não terá que repetir tipos longos e complicados (DRY, tanto quanto possível). Por exemplo, ConditionalRange::end:

iterator_type end() const { return { range.end(), range.end(), condition }; } 

Além disso, você provavelmente deseja que sua função também aceite matrizes de estilo C. Portanto, você deve usar std::begin e std::end em vez de chamar as funções de membro:

iterator_type end() const { return { std::end(range), std::end(range), condition }; } 

Com isso (em todos os lugares em seu código) e algumas mudanças triviais, seu código também funcionará para C- arrays de estilo (testado aqui e funciona bem).

Nomeando sua função

O nome makeConditionalRange é bastante longo. A função que você criou já existe em outras bibliotecas e linguagens de programação (Python vem à minha mente) com o nome filter. mudar seu nome para que muitos usuários reconheçam o nome à primeira vista (além disso, será mais curto).

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *