C ++ 표준 라이브러리에 transform_if가없는 이유는 무엇입니까?


82

조건부 복사 (1.로 수행 가능)를 수행하고 copy_if싶지만 값 컨테이너에서 해당 값에 대한 포인터 컨테이너 (2. 로 수행 가능) 로의 사용 사례가 나타났습니다 transform.

사용 가능한 도구로 는 두 단계 미만으로 수행 할 수 없습니다 .

#include <vector>
#include <algorithm>

using namespace std;

struct ha { 
    int i;
    explicit ha(int a) : i(a) {}
};

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector
    vector<ha*> pv; // temporary vector
    // 1. 
    transform(v.begin(), v.end(), back_inserter(pv), 
        [](ha &arg) { return &arg; }); 
    // 2. 
    copy_if(pv.begin(), pv.end(), back_inserter(ph),
        [](ha *parg) { return parg->i < 2;  }); // 2. 

    return 0;
}

우리가 부를 수있는 당연히 remove_ifpv임시의 필요성을 제거, 더 나은 아직하지만, 그것은 할 어렵지 않다 구현 이 같은 (단항 작업) :

template <
    class InputIterator, class OutputIterator, 
    class UnaryOperator, class Pred
>
OutputIterator transform_if(InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperator op, Pred pred)
{
    while (first1 != last1) 
    {
        if (pred(*first1)) {
            *result = op(*first1);
            ++result;
        }
        ++first1;
    }
    return result;
}

// example call 
transform_if(v.begin(), v.end(), back_inserter(ph), 
[](ha &arg) { return &arg;      }, // 1. 
[](ha &arg) { return arg.i < 2; });// 2.
  1. 사용 가능한 C ++ 표준 라이브러리 도구에 대한보다 우아한 해결 방법이 있습니까?
  2. transform_if도서관에 존재하지 않는 이유 가 있나요? 기존 도구의 조합이 충분한 해결 방법이고 / 또는 성능이 현명한 것으로 간주됩니까?

(IMO) 이름 transform_if은 "특정 술어가 충족되는 경우에만 변환"을 의미합니다. 당신이 원하는 것에 대한 더 설명적인 이름이 될 것입니다 copy_if_and_transform!
Oliver Charlesworth 2014 년

@OliCharlesworth는 실제로 copy_if"특정 술어가 충족되는 경우에만 복사"를 의미합니다. 똑같이 모호합니다.
Shahbaz

@Shahbaz :하지만 그게 copy_if뭐죠?
Oliver Charlesworth 2014 년

2
그런 일의 이름에 대한 논쟁이 그것을 구현하지 않은 실제 이유 였다면 나는 놀라지 않을 것입니다!
Nikos Athanasiou

6
이 주석에서 뭔가 빠졌을 수도 있지만 transform_if변환이 다른 호환되지 않는 유형으로 될 수 있다면 변환하지 않는 요소를 어떻게 복사 할 수 있습니까? 질문의 구현은 정확히 그러한 기능이 수행 할 것으로 기대하는 것입니다.

답변:


32

표준 라이브러리는 기본 알고리즘을 선호합니다.

컨테이너와 알고리즘은 가능하면 서로 독립적이어야합니다.

마찬가지로 기존 알고리즘으로 구성 할 수있는 알고리즘은 속기처럼 거의 포함되지 않습니다.

변환이 필요한 경우 간단하게 작성할 수 있습니다. / today /를 원하고 기성품으로 구성하고 오버 헤드를 발생시키지 않으려면 Boost.Range 와 같은 게으른 범위 가있는 범위 라이브러리를 사용할 수 있습니다. 예 :

v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0)

@hvd가 주석에서 지적했듯이 transform_if이중 결과는 다른 유형 ( double이 경우)이됩니다. 구성 순서가 중요하며 Boost Range를 사용하면 다음과 같이 작성할 수도 있습니다.

 v | transformed(arg1 * arg1 / 7.0) | filtered(arg1 < 2.0)

결과적으로 다른 의미가 생깁니다. 이것은 요점을 집으로 가져옵니다.

그것은 거의 의미가 포함 std::filter_and_transform, std::transform_and_filter, std::filter_transform_and_filter표준 라이브러리로 등 등 .

Live On Coliru 샘플보기

#include <boost/range/algorithm.hpp>
#include <boost/range/adaptors.hpp>

using namespace boost::adaptors;

// only for succinct predicates without lambdas
#include <boost/phoenix.hpp>
using namespace boost::phoenix::arg_names;

// for demo
#include <iostream>

int main()
{
    std::vector<int> const v { 1,2,3,4,5 };

    boost::copy(
            v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0),
            std::ostream_iterator<double>(std::cout, "\n"));
}

26
문제는 표준 알고리즘이 게으르지 않기 때문에 쉽게 구성 할 수 없다는 것입니다.
Jan Hudec 2014 년

1
@JanHudec 정말. (미안합니다? :)). 이것이 바로 라이브러리를 사용하는 이유입니다 (동시성을 위해 AMP / TBB를 사용하거나 C #에서 Reactive Extensions를 사용하는 것과 유사). 많은 사람들이 표준에 포함시키기 위해 범위 제안 + 구현 작업을하고 있습니다.
sehe

2
@sehe +1 매우 인상적입니다. 오늘 새로운 것을 배웠습니다! Boost.Range와 Phoenix에 익숙하지 않은 사람에게 boost::phoenix람다없이 멋진 술어를 만드는 방법을 설명하는 문서 / 예제를 찾을 수있는 곳을 알려주 시겠습니까? 빠른 Google 검색에서 관련성이없는 것으로 나타났습니다. 감사!
Ali는

1
"std :: filter_and_transform"부분을 포함하는 것은 거의 의미가 없습니다. 다른 프로그래밍 언어도 "표준 라이브러리"에서이 조합을 제공합니다. 요소 목록을 한 번 반복하여 즉시 변환하고 변환 할 수없는 요소는 건너 뛰는 것이 합리적입니다. 다른 접근 방식에는 하나 이상의 패스가 필요합니다. 예, BOOST를 사용할 수 있지만 실제로 질문은 "C ++ 표준 라이브러리에 transform_if가없는 이유는 무엇입니까?"였습니다. 그리고 IMHO, 그는 이것을 의심 할 권리가 있습니다. 표준 라이브러리에는 이러한 기능이 있어야합니다.
Jonny Dee

1
@sehe "그들은 모두 컴포저 블 추상화를 사용한다"에 관해서 : 그것은 사실이 아닙니다. 예를 들어 Rust는 정확히 그러한 transform_if. 라고 filter_map합니다. 그러나 코드를 단순화하기 위해 거기에 있음을 인정해야하지만 반면에 C ++ 케이스에서 동일한 인수를 적용 할 수 있습니다.
Jonny Dee

6

새로운 for 루프 표기법은 여러면에서 컬렉션의 모든 요소에 액세스하는 알고리즘의 필요성을 줄여줍니다. 이제 루프를 작성하고 논리를 제자리에 두는 것이 더 깔끔해졌습니다.

std::vector< decltype( op( begin(coll) ) > output;
for( auto const& elem : coll )
{
   if( pred( elem ) )
   {
        output.push_back( op( elem ) );
   }
}

지금 알고리즘에 넣을 수있는 많은 가치를 제공합니까? 예, 알고리즘은 C ++ 03에 유용했을 것이고 실제로 저는 그것을 위해 하나를 가지고 있었지만 지금은 필요하지 않으므로 추가 할 때 실질적인 이점이 없습니다.

실제 사용에서 코드가 항상 정확히 그런 것은 아닙니다. "op"및 "pred"함수가 반드시 있어야하는 것은 아니며 알고리즘에 "적합"되도록하기 위해 람다를 만들어야 할 수도 있습니다. 논리가 복잡한 경우 문제를 분리하는 것이 좋지만 입력 유형에서 멤버를 추출하여 값을 확인하거나 컬렉션에 추가하는 문제라면 알고리즘을 사용하는 것보다 훨씬 간단합니다.

또한 어떤 종류의 transform_if를 추가하면 변환 전후에 술어를 적용할지 아니면 2 개의 술어를 두 위치에 모두 적용할지 여부를 결정해야합니다.

그래서 우리는 무엇을할까요? 3 개의 알고리즘을 추가 하시겠습니까? (컴파일러가 변환의 양쪽 끝에 술어를 적용 할 수있는 경우 사용자는 실수로 잘못된 알고리즘을 쉽게 선택할 수 있으며 코드는 여전히 컴파일되지만 잘못된 결과를 생성합니다).

또한 컬렉션이 큰 경우 사용자가 반복기를 사용하거나 매핑 / 축소를 원합니까? map / reduce를 도입하면 방정식이 훨씬 더 복잡해집니다.

기본적으로 라이브러리는 도구를 제공하며 사용자는 알고리즘의 경우와는 달리 다른 방식으로 수행하지 않고 원하는 작업에 맞게 도구를 사용합니다. (위의 사용자가 실제로하고 싶은 일에 맞추기 위해 accumulate를 사용하여 일을 어떻게 비틀려고 시도했는지보십시오).

간단한 예를 들어,지도. 각 요소에 대해 키가 짝수이면 값을 출력합니다.

std::vector< std::string > valuesOfEvenKeys
    ( std::map< int, std::string > const& keyValues )
{
    std::vector< std::string > res;
    for( auto const& elem: keyValues )
    {
        if( elem.first % 2 == 0 )
        {
            res.push_back( elem.second );
        }
    }
    return res;
}         

멋지고 간단합니다. transform_if 알고리즘에 적합합니까?


3
위의 코드에 람다가 2 개있는 transform_if보다 오류의 여지가 더 많다고 생각한다면 하나는 술어 용이고 다른 하나는 변환 용입니다. 어셈블리, C 및 C ++는 서로 다른 언어이며 위치가 다릅니다. 알고리즘이 루프에 비해 유리할 수있는 유일한 위치는 "매핑 / 축소"기능이므로 대규모 컬렉션에서 동시에 실행됩니다. 그러나 이렇게하면 사용자가 순서대로 반복할지 맵 축소할지 여부를 제어 할 수 있습니다.
CashCow

3
적절한 기능적 접근에서 술어와 뮤 테이터에 대한 함수는 구조를 적절하게 구조화하는 잘 정의 된 블록입니다. For 루프 본문에는 임의의 항목이 포함될 수 있으며 모든 루프는 그 동작을 이해하기 위해 신중하게 분석되어야합니다.
Bartek Banachewicz 2015

2
적절한 기능적 언어에 대한 적절한 기능적 접근 방식을 남겨주세요. 이것은 C ++입니다.
CashCow

3
"transform_if 알고리즘에 적합합니까?" 즉 이다 그것은 모든 하드 코딩을 제외하고는 "transform_if 알고리즘".
R. Martinho Fernandes 2015

2
transform_if와 동일한 기능을 수행합니다. 그 알고리즘은 코드를 단순화하거나 어떤 식 으로든 개선하기위한 것이지 더 복잡하게 만드는 것이 아닙니다.
CashCow

5

오랜만에이 질문을 되살려 서 죄송합니다. 최근에 비슷한 요구 사항이있었습니다. 부스트를 취하는 back_insert_iterator 버전을 작성하여 해결했습니다.

template<class Container>
struct optional_back_insert_iterator
: public std::iterator< std::output_iterator_tag,
void, void, void, void >
{
    explicit optional_back_insert_iterator( Container& c )
    : container(std::addressof(c))
    {}

    using value_type = typename Container::value_type;

    optional_back_insert_iterator<Container>&
    operator=( const boost::optional<value_type> opt )
    {
        if (opt) {
            container->push_back(std::move(opt.value()));
        }
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator*() {
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator++() {
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator++(int) {
        return *this;
    }

protected:
    Container* container;
};

template<class Container>
optional_back_insert_iterator<Container> optional_back_inserter(Container& container)
{
    return optional_back_insert_iterator<Container>(container);
}

다음과 같이 사용 :

transform(begin(s), end(s),
          optional_back_inserter(d),
          [](const auto& s) -> boost::optional<size_t> {
              if (s.length() > 1)
                  return { s.length() * 2 };
              else
                  return { boost::none };
          });

1
측정되지 않음-사용자가 자신의 경험이 CPU와 관련이 있다고 불평 할 때까지 (즉, 절대로) 나노초보다 정확성에 더 관심이 있습니다. 그러나 나는 그것이 가난하다는 것을 볼 수 없습니다. 옵션은 메모리 할당이없고 Ts 생성자는 옵션이 실제로 채워진 경우에만 호출되므로 매우 저렴합니다. 모든 코드 경로가 컴파일 타임에 표시되므로 옵티마이 저가 거의 모든 데드 코드를 제거 할 것으로 기대합니다.
Richard Hodges 2015

네. 나는 그것이 범용 알고리즘에 관한 것이 아니라면 동의 할 것입니다 (실제로는 그것들 내부의 일반적인 빌딩 블록). 이것은 내가 얻는 것처럼 단순하지 않는 한 일반적으로 열광하지 않는 곳입니다. 또한, 임의의 출력 반복자에 대한 데코레이터가되는 선택적 처리를 원합니다 (최소한 출력 반복자의 구성 가능성을 얻지 만 알고리즘의 구성 가능성 부족을 막으려 고합니다).
sehe 2015

iteratior의 데코레이터를 통해 선택적 삽입을 처리하든 변환 함수에서 처리하든 논리적으로 차이가 없습니다. 궁극적으로 플래그 테스트 일뿐입니다. 최적화 된 코드는 어느 쪽이든 동일하다는 것을 알게 될 것입니다. 전체 최적화를 방해하는 유일한 방법은 예외 처리입니다. T를 noexcept 생성자로 표시하면이 문제를 해결할 수 있습니다.
리처드 스

transform () 호출은 어떤 형식을 취 하시겠습니까? 나는 우리가 컴포저 블 이터레이터 스위트를 만들 수있을 것이라고 확신합니다.
Richard Hodges 2015

나도 :) 나는 당신의 제안에 대해 언급했습니다. 나는 (내가 오래 전에 그 있었다의 대신 범위 및 작성 가능 알고리즘을 보자 :).) 다른 뭔가를 제안되지 않았다
sehe

3

표준은 중복을 최소화하는 방식으로 설계되었습니다.

이 특별한 경우 간단한 range-for 루프를 사용하여 더 읽기 쉽고 간결한 방식으로 알고리즘의 목표를 달성 할 수 있습니다.

// another way

vector<ha*> newVec;
for(auto& item : v) {
    if (item.i < 2) {
        newVec.push_back(&item);
    }
}

나는 컴파일하고, 몇 가지 진단을 추가하고, OP의 알고리즘과 광산을 나란히 제시하도록 예제를 수정했습니다.

#include <vector>
#include <algorithm>
#include <iostream>
#include <iterator>

using namespace std;

struct ha { 
    explicit ha(int a) : i(a) {}
    int i;   // added this to solve compile error
};

// added diagnostic helpers
ostream& operator<<(ostream& os, const ha& t) {
    os << "{ " << t.i << " }";
    return os;
}

ostream& operator<<(ostream& os, const ha* t) {
    os << "&" << *t;
    return os;
}

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector
    vector<ha*> pv; // temporary vector
    // 1. 
    transform(v.begin(), v.end(), back_inserter(pv), 
        [](ha &arg) { return &arg; }); 
    // 2. 
    copy_if(pv.begin(), pv.end(), back_inserter(ph),
        [](ha *parg) { return parg->i < 2;  }); // 2. 

    // output diagnostics
    copy(begin(v), end(v), ostream_iterator<ha>(cout));
    cout << endl;
    copy(begin(ph), end(ph), ostream_iterator<ha*>(cout));
    cout << endl;


    // another way

    vector<ha*> newVec;
    for(auto& item : v) {
        if (item.i < 2) {
            newVec.push_back(&item);
        }
    }

    // diagnostics
    copy(begin(newVec), end(newVec), ostream_iterator<ha*>(cout));
    cout << endl;
    return 0;
}

3

얼마 후이 질문을 다시 찾고 잠재적으로 유용한 일반 반복기 어댑터를 모두 고안 한 후 원래 질문에는 std::reference_wrapper.

포인터 대신 사용하면 좋습니다.

Live On Coliru

#include <algorithm>
#include <functional> // std::reference_wrapper
#include <iostream>
#include <vector>

struct ha {
    int i;
};

int main() {
    std::vector<ha> v { {1}, {7}, {1}, };

    std::vector<std::reference_wrapper<ha const> > ph; // target vector
    copy_if(v.begin(), v.end(), back_inserter(ph), [](const ha &parg) { return parg.i < 2; });

    for (ha const& el : ph)
        std::cout << el.i << " ";
}

인쇄물

1 1 

1

copy_if함께 사용할 수 있습니다 . 왜 안돼? 정의 OutputIt( 사본 참조 ) :

struct my_inserter: back_insert_iterator<vector<ha *>>
{
  my_inserter(vector<ha *> &dst)
    : back_insert_iterator<vector<ha *>>(back_inserter<vector<ha *>>(dst))
  {
  }
  my_inserter &operator *()
  {
    return *this;
  }
  my_inserter &operator =(ha &arg)
  {
    *static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;
    return *this;
  }
};

코드를 다시 작성하십시오.

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector

    my_inserter yes(ph);
    copy_if(v.begin(), v.end(), yes,
        [](const ha &parg) { return parg.i < 2;  });

    return 0;
}

3
"왜 안돼?" -코드는 인간을위한 것입니다. 나에게 마찰은 실제로 람다 대신 함수 객체를 작성하는 것보다 더 나쁩니다. *static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;읽을 수없고 불필요하게 구체적입니다. 보다 일반적인 사용법 으로이 C ++ 17 테이크를 참조하십시오 .
sehe dec.

다음은 기본 반복자를 하드 코딩하지 않는 버전입니다 ( 예를 들어 std::insert_iterator<>또는 함께 사용할 수 있음 std::ostream_iterator<>). 또한 변환 (예 : 람다)을 제공 할 수 있습니다. c ++ 17, 유용 해 보이기 시작 / C ++ 11에서 동일
sehe dec

이 시점에서 기본 반복자를 유지할 이유가 거의 없으며 간단하게 다음과 같이 할 수 있습니다. Boost에 더 나은 구현 인 boost :: function_output_iterator 가 포함되어 있다는 점에 주목하면서 아무 함수사용할 수 있습니다 . 이제 남은 것은 재창조입니다. :)for_each_if
sehe dec

사실, 원래 질문을 다시 읽으면서 C ++ 11 표준 라이브러리를 사용하여 이유목소리를 추가해 보겠습니다 .
sehe dec.

0
template <class InputIt, class OutputIt, class BinaryOp>
OutputIt
transform_if(InputIt it, InputIt end, OutputIt oit, BinaryOp op)
{
    for(; it != end; ++it, (void) ++oit)
        op(oit, *it);
    return oit;
}

사용법 : (CONDITION 및 TRANSFORM은 매크로가 아니며 적용하려는 조건 및 변환에 대한 자리 표시 자입니다.)

std::vector a{1, 2, 3, 4};
std::vector b;

return transform_if(a.begin(), a.end(), b.begin(),
    [](auto oit, auto item)             // Note the use of 'auto' to make life easier
    {
        if(CONDITION(item))             // Here's the 'if' part
            *oit++ = TRANSFORM(item);   // Here's the 'transform' part
    }
);

이 구현 프로덕션이 준비되어 있다고 평가 하시겠습니까? 복사 할 수없는 요소와 잘 작동합니까? 아니면 이동 반복자?
sehe

0

이것은 질문 1 "사용 가능한 C ++ 표준 라이브러리 도구에보다 우아한 해결 방법이 있습니까?"에 대한 대답입니다.

C ++ 17을 사용할 수 있다면 std::optionalC ++ 표준 라이브러리 기능 만 사용하는 더 간단한 솔루션에 사용할 수 있습니다 . 아이디어는 std::nullopt매핑이없는 경우 반환 하는 것입니다.

Coliru에서 라이브보기

#include <iostream>
#include <optional>
#include <vector>

template <
    class InputIterator, class OutputIterator, 
    class UnaryOperator
>
OutputIterator filter_transform(InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperator op)
{
    while (first1 != last1) 
    {
        if (auto mapped = op(*first1)) {
            *result = std::move(mapped.value());
            ++result;
        }
        ++first1;
    }
    return result;
}

struct ha { 
    int i;
    explicit ha(int a) : i(a) {}
};

int main()
{
    std::vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector

    // GOAL : make a vector of pointers to elements with i < 2
    std::vector<ha*> ph; // target vector
    filter_transform(v.begin(), v.end(), back_inserter(ph), 
        [](ha &arg) { return arg.i < 2 ? std::make_optional(&arg) : std::nullopt; });

    for (auto p : ph)
        std::cout << p->i << std::endl;

    return 0;
}

방금 구현했습니다. 여기서 C ++로 Rust의 접근 방식 을 했습니다.


0

std::accumulate대상 컨테이너에 대한 포인터에서 작동하는 것을 사용할 수 있습니다 .

Live On Coliru

#include <numeric>
#include <iostream>
#include <vector>

struct ha
{
    int i;
};

// filter and transform is here
std::vector<int> * fx(std::vector<int> *a, struct ha const & v)
{
    if (v.i < 2)
    {
        a->push_back(v.i);
    }

    return a;
}

int main()
{
    std::vector<ha> v { {1}, {7}, {1}, };

    std::vector<int> ph; // target vector

    std::accumulate(v.begin(), v.end(), &ph, fx);
    
    for (int el : ph)
    {
        std::cout << el << " ";
    }
}

인쇄물

1 1 
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.