Checking if a value is in a container
Neat idea.
template&lt;typename T1, template &lt;typename&gt; typename Container, typename T2&gt;
bool contains (Container&lt;T1&gt; const &amp;source, T2 const &amp;val)

I’m not sure a template template parameter is necessary here, or even a good idea. It prevents the function from being used with containers that aren’t templates… or that aren’t the kind of templates you expect.
There is a standard definition for “container”. (There’s also a more-or-less matching proposed concept in the range-v3 library.) Under that definition you could get T1 as typename Container::value_type.
if constexpr (std::is_same&lt;T1, T2&gt;::value)

I don’t think is_same is the test you really want to be applying here, because it precludes all kinds of useful cases. For example, let’s say you have a vector of strings, and the key you want to search for is a string_view:
auto strings = std::vector&lt;std::string&gt;{/*...*/};

auto const search_key = &quot;to be found&quot;sv;

// This won't work
if (contains(strings, search_key))
    // ...

What you want is really the C++20 concept std::equality_comparable_with. Lacking concepts in C++17, you could roll a type trait that does the same test.
return std::find(std::cbegin(source), std::cend(source), val) != std::cend(source);

Using the fully-qualified forms of std::cbegin and std::cend prevents ADL, which could break basically any container not defined in the std namespace.
You should use the standard pattern:
using std::cbegin;
using std::cend;

return std::find(cbegin(source), cend(source), val) != cend(source);

Also, I’d suggest using begin and end rather than cbegin and cend. Why? Because it doesn’t matter, since source is const, but third-party containers may not have bothered to add support for cbegin and cend (begin and end should be universal).
I think you really need some additional type traits (or concepts, if you’re targeting C++20) to make this work. First, a type trait is_container would be very handy for making the function produce better errors, and for helping with the internals:
template &lt;typename Container, typename T&gt;
auto contains(Container const&amp; source, T const&amp; val) -&gt;
    std::enable_if_t&lt;is_container_v&lt;Container&gt;, bool&gt;

Or perhaps, rather than using SFINAE, you could use a static_assert:
template &lt;typename Container, typename T&gt;
auto contains(Container const&amp; source, T const&amp; val) -&gt; bool
{
    static_assert(is_container_v&lt;Container&gt;);

You could also use an equality_comparable_with trait, to allow for more flexibility:
template &lt;typename Container, typename T&gt;
auto contains(Container const&amp; source, T const&amp; val) -&gt; bool
{
    if constexpr (is_equality_comparable_with_v&lt;typename Container::value_type, T&gt;)
        // ...

Those two type traits will also help when testing for whether recursion is possible.
template &lt;typename Container, typename T&gt;
auto contains(Container const&amp; source, T const&amp; val) -&gt; bool
{
    if constexpr (is_equality_comparable_with_v&lt;typename Container::value_type, T&gt;)
        // just use std::find
    else if constexpr (is_container_v&lt;typename Container::value_type&gt;)
        // recurse
    else
        // error

Those type traits will help a lot when there are problems; they can make a huge difference in the size and readability of the error messages you get if you’re dealing with deeply nested containers.
Other than that, the only other thing I can think to suggest is to make the function constexpr. Because why not?
Questions
You say &quot;containers that aren’t templates&quot; and &quot; templates you expect&quot;, can you add 1 or 2 examples of this?
In order for a container-like type to work with your template template parameter, 3 things must be true:

It must be a template.
It must have 1 template type parameter (with any subsequent template parameters aliased or defaulted away).
That parameter must be the same as the container’s value type.

For an example that fails the first case: std::filesystem::directory_iterator. It is not a template. So this works:
auto dir = std::filesystem::directory_iterator(&quot;.&quot;);
if (std::find(begin(dir), end(dir), &quot;foo&quot;) != end(dir))
    std::cout &lt;&lt; &quot;foo is in current working directory&quot;;

but this won’t work:
auto dir = std::filesystem::directory_iterator(&quot;.&quot;);
if (gco::contains(dir, &quot;foo&quot;))
    std::cout &lt;&lt; &quot;foo is in current working directory&quot;;

For an example that fails the second case: std::array. Your function expects a template with a single type parameter (and anything else defaulted away). std::array has a type parameter… and a non-type parameter (which is not defaulted). It’s certainly a container, and a template, but not the kind of template you expect.
So this works:
auto numbers = std::array&lt;int, 5&gt;{1, 2, 3, 4, 5};
if (std::find(begin(numbers), end(numbers), 3) != end(numbers))
    std::cout &lt;&lt; &quot;found 3&quot;;

but this won’t work:
auto numbers = std::array&lt;int, 5&gt;{1, 2, 3, 4, 5};
if (gco::contains(numbers, 3))
    std::cout &lt;&lt; &quot;found 3&quot;;

For an example that fails the third case… I can’t think of anything off the top of my head. This would require a container template whose first template parameter is not the container’s value type. That would be an odd thing to do (there’s certainly nothing like that in the standard library)… but it could happen!
You suggest to use the standard definition for container. Can you show me how this is done?
In a perfect world, the standard library would have done this for you. (And it will do it for you, but not until C++20.)
But if we’re going to get into this, we really need to tighten up some terminology. I don’t think you want contains() to be a container operation. I think you want it to be a range operation.
In C++ lingo, a container is a type that holds a bunch of stuff. std::vector, std::list, std::map, and so on, they’re all containers. They own their contents; when the container is destroyed, so are all the contents.
But there are other things that are container-like but not containers. As of C++17, there are also views.
A view is a type that provides a container-like view of stuff, but does not hold that stuff. As of C++17, the only view in the standard library (unless I’m forgetting something) is std::string_view, though there are lot more in the pipes, like C++20’s std::span. std::string_view does not own its contents. By contrast, std::string, which is a container, does.
I’m pretty sure you’d want gco::contains() to work with std::string_view. So obviously you want to support more than just containers.
And there are still other things that are neither containers nor views, but still seem like they should be able to work with gco::contains(). There’s nothing really in the standard library yet, but there’s a lot coming in future. For example, coroutines would bring generators. I can imagine a function that checks whether a number is part of the Fibonacci sequence that works something like this:
// fibonacci_sequence() is a generator that returns successive
// elements in the Fibonacci sequence: 1, 2, 3, 5, 8, 13, ...
if (gco::contains(fibonacci_sequence(), number))
    std::cout &lt;&lt; number &lt;&lt; &quot; is in the Fibonacci sequence&quot;;

The generic C++ concept that covers both containers and views and more (including types that provides a container-like view of stuff, but not only don’t own the stuff, the stuff might not actually exist) is range. C++20 will be bringing a long-awaited range library, but C++ has been taking baby steps to support ranges since all the way back in C++11, with the range-for-loop.
I think what you’re making is not a library of generalized container operations, but rather a range library.
Now, if this were C++20, everything would be trivial. You could just do:
template &lt;typename&gt; struct dependent_false : std::false_type {};

template &lt;typename T&gt;
constexpr auto dependent_false_v = dependent_false&lt;T&gt;::value;

template &lt;std::input_range Container, typename T&gt;
constexpr auto contains(Container const&amp; source, T const&amp; val) -&gt; bool
{
    using value_type = std::ranges::range_value_t&lt;Container&gt;;

    using std::begin;
    using std::end;

    if constexpr (std::equality_comparable_with&lt;value_type, T&gt;)
        return std::find(begin(source), end(source), val) != end(source);
    else if constexpr (std::input_range&lt;T&gt;)
        return std::any_of(begin(source), end(source), [&amp;val](auto&amp;&amp; el) { return contains(el, val); });
    else
        static_assert(dependent_false_v&lt;Container&gt;);
}

Unfortunately, in C++17, you don’t have concepts, so you have to roll your own type traits to do the tests.
Okay, so… how? Well, the first thing you need to do is figure out what the requirements are. A minimal range is anything that works with range-for. That requires at least:

begin()
end()
that the return types of begin() and end() can be equality-compared
that the return type of begin() supports operator++; and
probably a few other things I’m forgetting.

You can test the first two requirements with something like:
template &lt;typename Range&gt;
constexpr auto adl_begin(Range&amp;&amp; r)
{
    using std::begin;
    return begin(r);
}

template &lt;typename Range&gt;
constexpr auto adl_end(Range&amp;&amp; r)
{
    using std::end;
    return end(r);
}

template &lt;typename T, typename = void&gt;
struct has_begin_support : std::false_type {};

template &lt;typename T&gt;
struct has_begin_support&lt;T, std::void_t&lt;decltype(adl_begin(std::declval&lt;T&gt;()))&gt; : std::true_type {};

template &lt;typename T, typename = void&gt;
struct has_end_support : std::false_type {};

template &lt;typename T&gt;
struct has_end_support&lt;T, std::void_t&lt;decltype(adl_end(std::declval&lt;T&gt;()))&gt; : std::true_type {};

Then make similar type traits to check that the return type of begin() is equality comparable with the return type of end(), and that the return type of begin() is an input iterator, and so on… and eventually put it all together as a single type trait.
Yes, that’s a shitload of work (unless you use a library like range-v3 which I HIGHLY recommend), but that’s why C++ programmers get the big bucks.
Now, this next bit is an opinionated rant:

 Honestly, I’ve started suggesting to programmers to consider targeting C++20, rather than C++17, even though C++20 isn’t fully officially out yet, and compiler support is as yet anemic. C++20 is the biggest update to C++ since C++11… and it might be even bigger. EVERYTHING will change once C++20 is the norm, probably even far more so than it did when C++11 came around. The amount of code you’d have to write to do contains() well in C++17 is dozens and dozens, if not hundreds of lines of arcane SFINAE crap that only experts understand with bullshit like enable_if and void_t. In C++20? It’s like, 10-12 lines, all obvious, simple, and brief (well, except maybe for the dependent_false trick, but that might not even be necessary, depending on how clear compiler errors are in C++20 mode).

 So it might be worth your while to hold off on writing a container ops library, and for now spend the time reading up on what’s coming in C++: like concepts, modules, and particularly the ranges library. Then when you’re ready, hopefully compiler support will have caught up, and you can roll out this library easily.

 If you really want to target C++17 with your library, that’s fine—there’s very little you can’t do in C++17 (it’s just MUCH easier in C++20). Just understand that you will be diving into a deep, deep well of very, very complicated stuff, and—and this is the worst part—even if you just want to do it for the learning experience, it will be mostly a waste of time, because the skills you’ll learn will be obsolete in a few months.

you suggest either using 'std::enable_if_t' or a static_assert. Is one of the two preferred or more common?
That’s an engineering question, meaning the answer is: there is no right answer; it depends on what you want.
Both methods will trigger a compile error if the constraints are not met. The difference is how.
static_assert is easier to understand. If the constraint fails, the compile just dies. That’s it. Nothing fancy. It’s so brutally simple, that it actually requires tricks to make it not fail when you don’t want it to. If you look at the C++20 version of contains() I wrote above, the static assert uses dependent_false:
static_assert(dependent_false_v&lt;Container&gt;);

If I’d just done:
static_assert(false);

it would just fail, always. Literally always—no code that uses contains() would compile. “But,” you say, “that static_assert is in the else part of an if constexpr!” Doesn’t matter. static_assert doesn’t care. When that function is compiled, if the compiler sees static_assert(false), it just dies immediately, before even bothering to check the rest of the function, before even realizing which branch of the if constexpr it’s in. The dependent_false trick is a way to confuse the compiler temporarily: it doesn’t see the false until after it parses everything in the function… at which point it then sees it’s in an else that doesn’t apply, so it ignores the assertion.
enable_if is more complicated. What it does (assuming you use it right) is remove the function from the overload set temporarily.
To understand what that means, imagine you have two overloads of template &lt;typename T&gt; foo(T) with different return types, then you call foo(0) (with T as an int):
template &lt;typename T&gt;
auto foo(T) -&gt; T { std::cout &lt;&lt; &quot;int&quot;; return {}; }

template &lt;typename T&gt;
auto foo(T) -&gt; typename T::value_type { std::cout &lt;&lt; &quot;T::value_type&quot;; return {}; }

foo(0); // what happens?

Well, first the compiler collects all the possibilities. So the overload set consists of both foo() functions.
Then the compiler eliminates obvious failures. In this case, foo(0) can’t possibly be calling the overload that returns T::value_type… because int does not have a value_type subtype: int::value_type is nonsense. So that overload is eliminated, leaving only the overload that returns T.
With only one possibility left, the compiler accepts that foo(0) means the first overload. The program compiles, and prints “int”.
If you try to use a std::string (which has a value_type) instead of an int, both overloads are valid, and you’ll get an error because the call is ambiguous.
What enable_if does is gives you control over the elimination step. It basically says “only include this function in the set of potential overloads if the condition is true”. If the condition is false, then it’s as if the function doesn’t exist.
Why is that cool? Well consider if you did this:
template &lt;typename Container, typename T&gt;
auto contains(Container const&amp; source, T const&amp; val) -&gt; bool
{
    static_assert(is_container_v&lt;Container&gt;);

Then when you call contains(something, other) and something is not a container, the static_assert fires, and the whole compile dies. That may be exactly what you want.
But!… Maybe if something is not a container, there is some other overload of contains() that might be suitable (“contains” is a common name, after all!).
So if you did this:
template &lt;typename Container, typename T&gt;
auto contains(Container const&amp; source, T const&amp; val) -&gt;
    std::enable_if_t&lt;is_container_v&lt;Container&gt;, bool&gt;

(or used a concept; a concept basically does the same thing as enable_if), then called contains(something, other) where something is not a container, you don’t immediately get a compile failure. Instead, this function just… disappears. Like it was never written. The compiler goes on searching for all the other overloads of contains() (for everything that is not a container), and if it finds a match, then good! If not, well, then that’s an error as always.
So that’s the difference:

With static_assert, if someone calls contains() with inappropriate arguments, the compile just dies immediately, even if there are other (and better) possibilities that might work.
With enable_if, if someone calls contains() with inappropriate arguments… the compiler just pretends the function doesn’t exist, and keeps looking for other possibilities. If it finds one, it uses it. If not, then that’s an error as always.

Which one is preferred?  It depends on what you want.
If you can be reasonably sure that no one will ever name another function contains() that might be confused with your function (which is a perfectly reasonable assumption!), then static_assert is just fine. But if you think you might be sharing the name contains() with other stuff… maybe you might not want to trigger a hard error right away; maybe you might want your function to “step aside” to give other functions with the same name a chance.
Hope all this helps!