progman posted some useful monad links, and I should know better than to write Yet Another Monad Explanation in a comment. But since you're coming from a C++ background, I can maybe fine-tune this to you personally. If it doesn't make sense, look somewhere else. :-)

First: There are actually two questions hiding here: (1) why are monads interesting in general, and (2) why does Haskell use monads to represent side effects? I'm going to mostly focus on (1) first, because that helped me more. Other people may strongly prefer starting from (2). My explanation will work best for people who like to make a model; explanations that start with (2) will work best for people who like to start with practical examples.

First, let's start with a C++ template:

template <typename T> List { ... }

Here, we have a list which can contain values of type T. So List<string> is a list of strings, and List<int> is a list of integers. Now, any Lisp or Ruby hacker would tell you that lists are better with a 'map' function. 'map' is a function which takes a list and a transformation function, and builds a new list by applying the transformation to every element of the old list. This way, if you have a list of strings, and a function 'convert_string_to_integer', you can write:

List<int> new_list = list_of_strings.map(convert_string_to_integer);

...and you've got a list of converted integers. If you understand this, you're over half of the way to monads!

To get a monad, we need to add two more things. First, we need a way to create a new list out of a single element:

List<int> new_list = List::from_element(3); // Creates the list { 3 }

And then we need the interesting bit. Imagine that we have a list of lists of integers, of type List<List<int>>:

{ { 1, 2, 3 }, {}, { 4 }, { 5, 6 } }

We want to flatten this into a single List<int> by smooshing all the lists together:

{ 1, 2, 3, 4, 5, 6 }

This function might be called 'join' or 'flatten' or (in Haskell), you might combine 'map' and 'join' into a "map a function then join the result" function called 'bind'.

A monad is basically any templated type that supports 'map' and 'from_elem' and 'flatten'. (There are a few common sense restrictions about how these functions related to each other, too.)

Lots and lots of interesting types either fit this model well, or can be smooshed into fitting. This model covers Python's list comprehensions, and JavaScript's promises, and Rust's Option type, and 90+% of collection types, and many much weirder and cooler things.

...and this brings us to I/O and Haskell. And here's where my explanation won't go far enough, but it might "click" for you latter on. In Haskell, conceptually speaking, you don't really perform I/O. Instead, you construct instances of "IO a", which is equivalent to the hypothetical C++:

template <typename T> IoAction { ... }

...where IoAction does some I/O and returns a value of type T. Then once you've described an I/O action, you ask Haskell to perform it. The type I/O action supports all our monad functions: If you have an IoAction<string>, it's easy to turn it into an IoAction<int> by running the original I/O action and applying a function to the output. Similarly, you can define IoAction::from_elem to (1) do no actual I/O, and (2) just return the elem you gave it. And if somebody gives you an IoAction<IoAction<string>> (an IoAction that returns another IoAction), you can "flatten" it by running the outer IoAction, taking the IoAction it returns, and running that.

So if you have an action 'chooseFileName' of type IoAction<string> and function 'readFile' which takes a string as an argument and returns an IoAction<string> that reads in the specified file, you could write:

chooseFileName.map(readFile)

...and get an IoAction<IoAction<string>>, and if you flatten it:

chooseFileName.map(readFile).flatten()

...you get a new IoAction<string> that prompts for a filename and reads a file. Or you could write:

IoAction::from_elem("my_file.txt").map(readFile).flatten()

...and get an IoAction<string> that returns whatever is in "my_file.txt". And this is how Haskell I/O works. But you'll probably have to play with it for a while until all the pieces fit together; Haskell is mathy that way.